Merge "Use a strongly typed LogicalDisplayId for displayId(1/n)" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 0ccdf37..ab5d503 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -1043,20 +1043,12 @@
name: "device_policy_aconfig_flags",
package: "android.app.admin.flags",
container: "system",
- exportable: true,
srcs: [
"core/java/android/app/admin/flags/flags.aconfig",
],
}
java_aconfig_library {
- name: "device_policy_exported_aconfig_flags_lib",
- aconfig_declarations: "device_policy_aconfig_flags",
- defaults: ["framework-minus-apex-aconfig-java-defaults"],
- mode: "exported",
-}
-
-java_aconfig_library {
name: "device_policy_aconfig_flags_lib",
aconfig_declarations: "device_policy_aconfig_flags",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
diff --git a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
index a4a2e80..9b0f5c9 100644
--- a/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
+++ b/apex/blobstore/service/java/com/android/server/blob/BlobStoreManagerShellCommand.java
@@ -133,7 +133,7 @@
pw.println(" --tag: Tag of the blob to delete.");
pw.println("idle-maintenance");
pw.println(" Run idle maintenance which takes care of removing stale data.");
- pw.println("query-blob-existence [-b BLOB_ID]");
+ pw.println("query-blob-existence [-b BLOB_ID] [-u | --user USER_ID]");
pw.println(" Prints 1 if blob exists, otherwise 0.");
pw.println();
}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
index d59d430..ad54cd39 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java
@@ -491,8 +491,10 @@
* Returns a list of all currently-executing jobs.
* @hide
*/
- @SuppressWarnings("HiddenAbstractMethod")
- public abstract List<JobInfo> getStartedJobs();
+ @Nullable
+ public List<JobInfo> getStartedJobs() {
+ return null;
+ }
/**
* <b>For internal system callers only!</b>
@@ -501,8 +503,10 @@
* <p class="note">This is a slow operation, so it should be called sparingly.
* @hide
*/
- @SuppressWarnings("HiddenAbstractMethod")
- public abstract List<JobSnapshot> getAllJobSnapshots();
+ @Nullable
+ public List<JobSnapshot> getAllJobSnapshots() {
+ return null;
+ }
/**
* @hide
@@ -510,8 +514,8 @@
@RequiresPermission(allOf = {
android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
- @SuppressWarnings("HiddenAbstractMethod")
- public abstract void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer);
+ public void registerUserVisibleJobObserver(@NonNull IUserVisibleJobObserver observer) {
+ }
/**
* @hide
@@ -519,9 +523,10 @@
@RequiresPermission(allOf = {
android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
- @SuppressWarnings("HiddenAbstractMethod")
- public abstract void unregisterUserVisibleJobObserver(
- @NonNull IUserVisibleJobObserver observer);
+ public void unregisterUserVisibleJobObserver(
+ @NonNull IUserVisibleJobObserver observer) {
+
+ }
/**
* @hide
@@ -529,7 +534,7 @@
@RequiresPermission(allOf = {
android.Manifest.permission.MANAGE_ACTIVITY_TASKS,
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL})
- @SuppressWarnings("HiddenAbstractMethod")
- public abstract void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
- @Nullable String debugReason);
+ public void notePendingUserRequestedAppStop(@NonNull String packageName, int userId,
+ @Nullable String debugReason) {
+ }
}
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index e20f525..e489c1a 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -38,3 +38,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "thermal_restrictions_to_fgs_jobs"
+ namespace: "backstage_power"
+ description: "Apply thermal restrictions to FGS jobs."
+ bug: "315157163"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
index 3bb395f..ba8e3e8 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java
@@ -1375,8 +1375,10 @@
final JobServiceContext jsc = mActiveServices.get(i);
final JobStatus jobStatus = jsc.getRunningJobLocked();
- if (jobStatus != null && !jsc.isWithinExecutionGuaranteeTime()
- && restriction.isJobRestricted(jobStatus)) {
+ if (jobStatus != null
+ && !jsc.isWithinExecutionGuaranteeTime()
+ && restriction.isJobRestricted(
+ jobStatus, mService.evaluateJobBiasLocked(jobStatus))) {
jsc.cancelExecutingJobLocked(restriction.getStopReason(),
restriction.getInternalReason(),
JobParameters.getInternalReasonCodeDescription(
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index 5d1433c..384d786 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -310,7 +310,8 @@
* Note: do not add to or remove from this list at runtime except in the constructor, because we
* do not synchronize access to this list.
*/
- private final List<JobRestriction> mJobRestrictions;
+ @VisibleForTesting
+ final List<JobRestriction> mJobRestrictions;
@GuardedBy("mLock")
@VisibleForTesting
@@ -3498,8 +3499,6 @@
/**
* Check if a job is restricted by any of the declared {@link JobRestriction JobRestrictions}.
- * Note, that the jobs with {@link JobInfo#BIAS_FOREGROUND_SERVICE} bias or higher may not
- * be restricted, thus we won't even perform the check, but simply return null early.
*
* @param job to be checked
* @return the first {@link JobRestriction} restricting the given job that has been found; null
@@ -3508,13 +3507,9 @@
*/
@GuardedBy("mLock")
JobRestriction checkIfRestricted(JobStatus job) {
- if (evaluateJobBiasLocked(job) >= JobInfo.BIAS_FOREGROUND_SERVICE) {
- // Jobs with BIAS_FOREGROUND_SERVICE or higher should not be restricted
- return null;
- }
for (int i = mJobRestrictions.size() - 1; i >= 0; i--) {
final JobRestriction restriction = mJobRestrictions.get(i);
- if (restriction.isJobRestricted(job)) {
+ if (restriction.isJobRestricted(job, evaluateJobBiasLocked(job))) {
return restriction;
}
}
@@ -4221,6 +4216,7 @@
return curBias;
}
+ /** Gets and returns the adjusted Job Bias **/
int evaluateJobBiasLocked(JobStatus job) {
int bias = job.getBias();
if (bias >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE) {
@@ -5907,7 +5903,7 @@
if (isRestricted) {
for (int i = mJobRestrictions.size() - 1; i >= 0; i--) {
final JobRestriction restriction = mJobRestrictions.get(i);
- if (restriction.isJobRestricted(job)) {
+ if (restriction.isJobRestricted(job, evaluateJobBiasLocked(job))) {
final int reason = restriction.getInternalReason();
pw.print(" ");
pw.print(JobParameters.getInternalReasonCodeDescription(reason));
@@ -6240,7 +6236,7 @@
proto.write(JobSchedulerServiceDumpProto.JobRestriction.REASON,
restriction.getInternalReason());
proto.write(JobSchedulerServiceDumpProto.JobRestriction.IS_RESTRICTING,
- restriction.isJobRestricted(job));
+ restriction.isJobRestricted(job, evaluateJobBiasLocked(job)));
proto.end(restrictionsToken);
}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
index 7aab67a..555a118 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/JobRestriction.java
@@ -62,10 +62,11 @@
* fine with it).
*
* @param job to be checked
+ * @param bias job bias to be checked
* @return false if the {@link JobSchedulerService} should not schedule this job at the moment,
* true - otherwise
*/
- public abstract boolean isJobRestricted(JobStatus job);
+ public abstract boolean isJobRestricted(JobStatus job, int bias);
/** Dump any internal constants the Restriction may have. */
public abstract void dumpConstants(IndentingPrintWriter pw);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
index ef634b5..ba01113 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/restrictions/ThermalStatusRestriction.java
@@ -24,6 +24,7 @@
import android.util.IndentingPrintWriter;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.job.Flags;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.controllers.JobStatus;
@@ -85,7 +86,18 @@
}
@Override
- public boolean isJobRestricted(JobStatus job) {
+ public boolean isJobRestricted(JobStatus job, int bias) {
+ if (Flags.thermalRestrictionsToFgsJobs()) {
+ if (bias >= JobInfo.BIAS_TOP_APP) {
+ // Jobs with BIAS_TOP_APP should not be restricted
+ return false;
+ }
+ } else {
+ if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE) {
+ // Jobs with BIAS_FOREGROUND_SERVICE or higher should not be restricted
+ return false;
+ }
+ }
if (mThermalStatus >= UPPER_THRESHOLD) {
return true;
}
@@ -107,6 +119,17 @@
|| (mService.isCurrentlyRunningLocked(job)
&& mService.isJobInOvertimeLocked(job));
}
+ if (Flags.thermalRestrictionsToFgsJobs()) {
+ // Only let foreground jobs run if:
+ // 1. They haven't previously run
+ // 2. They're already running and aren't yet in overtime
+ if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE
+ && job.getJob().isImportantWhileForeground()) {
+ return job.getNumPreviousAttempts() > 0
+ || (mService.isCurrentlyRunningLocked(job)
+ && mService.isJobInOvertimeLocked(job));
+ }
+ }
if (priority == JobInfo.PRIORITY_HIGH) {
return !mService.isCurrentlyRunningLocked(job)
|| mService.isJobInOvertimeLocked(job);
@@ -114,6 +137,13 @@
return true;
}
if (mThermalStatus >= LOW_PRIORITY_THRESHOLD) {
+ if (Flags.thermalRestrictionsToFgsJobs()) {
+ if (bias >= JobInfo.BIAS_FOREGROUND_SERVICE) {
+ // No restrictions on foreground jobs
+ // on LOW_PRIORITY_THRESHOLD and below
+ return false;
+ }
+ }
// For light throttling, throttle all min priority jobs and all low priority jobs that
// aren't already running or have been running for long enough.
return priority == JobInfo.PRIORITY_MIN
diff --git a/cmds/bootanimation/FORMAT.md b/cmds/bootanimation/FORMAT.md
index 01e8fe1..da8331a 100644
--- a/cmds/bootanimation/FORMAT.md
+++ b/cmds/bootanimation/FORMAT.md
@@ -126,7 +126,7 @@
Use `zopflipng` if you have it, otherwise `pngcrush` will do. e.g.:
for fn in *.png ; do
- zopflipng -m ${fn}s ${fn}s.new && mv -f ${fn}s.new ${fn}
+ zopflipng -m ${fn} ${fn}.new && mv -f ${fn}.new ${fn}
# or: pngcrush -q ....
done
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 2437be8..e225c5b0 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -966,7 +966,6 @@
ctor public AttributionSource(int, @Nullable String, @Nullable String);
ctor public AttributionSource(int, @Nullable String, @Nullable String, @NonNull android.os.IBinder);
ctor public AttributionSource(int, @Nullable String, @Nullable String, @Nullable java.util.Set<java.lang.String>, @Nullable android.content.AttributionSource);
- ctor @FlaggedApi("android.permission.flags.attribution_source_constructor") public AttributionSource(int, int, @Nullable String, @Nullable String, @NonNull android.os.IBinder, @Nullable String[], @Nullable android.content.AttributionSource);
ctor @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public AttributionSource(int, int, @Nullable String, @Nullable String, @NonNull android.os.IBinder, @Nullable String[], int, @Nullable android.content.AttributionSource);
method public void enforceCallingPid();
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 3c402ca..0caea7f 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -114,6 +114,7 @@
import com.android.internal.graphics.ColorUtils;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ContrastColorUtil;
+import com.android.internal.util.NewlineNormalizer;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -3190,7 +3191,7 @@
return charSequence;
}
- return charSequence.toString().replaceAll("[\r\n]+", "\n");
+ return NewlineNormalizer.normalizeNewlines(charSequence.toString());
}
private static CharSequence removeTextSizeSpans(CharSequence charSequence) {
@@ -6490,7 +6491,8 @@
// visual regressions.
@SuppressWarnings("AndroidFrameworkCompatChange")
private boolean bigContentViewRequired() {
- if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S) {
+ if (!Flags.notificationExpansionOptional()
+ && mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.S) {
return true;
}
// Notifications with contentView and without a bigContentView, style, or actions would
@@ -6593,6 +6595,11 @@
* @hide
*/
public RemoteViews createCompactHeadsUpContentView() {
+ // Don't show compact heads up for FSI notifications.
+ if (mN.fullScreenIntent != null) {
+ return createHeadsUpContentView(/* increasedHeight= */ false);
+ }
+
if (mStyle != null) {
final RemoteViews styleView = mStyle.makeCompactHeadsUpContentView();
if (styleView != null) {
@@ -10350,7 +10357,7 @@
@Nullable
@Override
public RemoteViews makeCompactHeadsUpContentView() {
- // TODO(b/336228700): Apply minimal HUN treatment for Call Style.
+ // Use existing heads up for call style.
return makeHeadsUpContentView(false);
}
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index efd5a45..ef8501f 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -351,6 +351,12 @@
}
/** @hide */
+ public boolean isFreeform() {
+ return configuration.windowConfiguration.getWindowingMode()
+ == WindowConfiguration.WINDOWING_MODE_FREEFORM;
+ }
+
+ /** @hide */
@WindowConfiguration.ActivityType
public int getActivityType() {
return configuration.windowConfiguration.getActivityType();
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 8c4667f..9cf83b9 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -50,3 +50,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "backstage_power"
+ name: "gate_fgs_timeout_anr_behavior"
+ description: "Gate the new behavior where an ANR is thrown once an FGS times out."
+ bug: "339315145"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
index 46c9e78..9ef8b38 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -21,7 +21,6 @@
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
-import android.app.admin.flags.Flags;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
@@ -177,10 +176,6 @@
* provisioned into "affiliated" mode when on a Headless System User Mode device.
*
* <p>This mode adds a Profile Owner to all users other than the user the Device Owner is on.
- *
- * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
- * DPCs should set the value of attribute "headless-device-owner-mode" inside the
- * "headless-system-user" tag as "affiliated".
*/
public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1;
@@ -190,10 +185,6 @@
*
* <p>This mode only allows a single secondary user on the device blocking the creation of
* additional secondary users.
- *
- * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM},
- * DPCs should set the value of attribute "headless-device-owner-mode" inside the
- * "headless-system-user" tag as "single_user".
*/
@FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED)
public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2;
@@ -392,30 +383,17 @@
}
mSupportsTransferOwnership = true;
} else if (tagName.equals("headless-system-user")) {
- String deviceOwnerModeStringValue = null;
- if (Flags.headlessSingleUserCompatibilityFix()) {
- deviceOwnerModeStringValue = parser.getAttributeValue(
- null, "headless-device-owner-mode");
- }
- if (deviceOwnerModeStringValue == null) {
- deviceOwnerModeStringValue =
- parser.getAttributeValue(null, "device-owner-mode");
- }
+ String deviceOwnerModeStringValue =
+ parser.getAttributeValue(null, "device-owner-mode");
- if ("unsupported".equalsIgnoreCase(deviceOwnerModeStringValue)) {
+ if (deviceOwnerModeStringValue.equalsIgnoreCase("unsupported")) {
mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
- } else if ("affiliated".equalsIgnoreCase(deviceOwnerModeStringValue)) {
+ } else if (deviceOwnerModeStringValue.equalsIgnoreCase("affiliated")) {
mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
- } else if ("single_user".equalsIgnoreCase(deviceOwnerModeStringValue)) {
+ } else if (deviceOwnerModeStringValue.equalsIgnoreCase("single_user")) {
mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
} else {
- if (Flags.headlessSingleUserCompatibilityFix()) {
- Log.e(TAG, "Unknown headless-system-user mode: "
- + deviceOwnerModeStringValue);
- } else {
- throw new XmlPullParserException(
- "headless-system-user mode must be valid");
- }
+ throw new XmlPullParserException("headless-system-user mode must be valid");
}
}
}
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 83daa45..6da96c1 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -217,6 +217,16 @@
}
flag {
+ name: "disallow_user_control_stopped_state_fix"
+ namespace: "enterprise"
+ description: "Ensure DPM.setUserControlDisabledPackages() clears FLAG_STOPPED for the app"
+ bug: "330688482"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "esim_management_ux_enabled"
namespace: "enterprise"
description: "Enable UX changes for esim management"
@@ -303,24 +313,3 @@
purpose: PURPOSE_BUGFIX
}
}
-
-flag {
- name: "headless_single_user_compatibility_fix"
- namespace: "enterprise"
- description: "Fix for compatibility issue introduced from using single_user mode on pre-Android V builds"
- bug: "338050276"
- is_exported: true
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
- name: "headless_single_min_target_sdk"
- namespace: "enterprise"
- description: "Only allow DPCs targeting Android V to provision into single user mode"
- bug: "338588825"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 63ffaa0..50c7b7f 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -60,6 +60,13 @@
}
flag {
+ name: "notification_expansion_optional"
+ namespace: "systemui"
+ description: "Experiment to restore the pre-S behavior where standard notifications are not expandable unless they have actions."
+ bug: "339523906"
+}
+
+flag {
name: "keyguard_private_notifications"
namespace: "systemui"
description: "Fixes the behavior of KeyguardManager#setPrivateNotificationsAllowed()"
diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig
index c7b168a..04c3686 100644
--- a/core/java/android/app/usage/flags.aconfig
+++ b/core/java/android/app/usage/flags.aconfig
@@ -47,3 +47,14 @@
description: "Feature flag for collecting app data size by file type API"
bug: "294088945"
}
+
+flag {
+ name: "disable_idle_check"
+ namespace: "backstage_power"
+ description: "disable idle check for USER_SYSTEM during boot up"
+ is_fixed_read_only: true
+ bug: "337864590"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java
index b070742..7f01a82 100644
--- a/core/java/android/content/AttributionSource.java
+++ b/core/java/android/content/AttributionSource.java
@@ -162,17 +162,6 @@
/** @hide */
@TestApi
- @FlaggedApi(Flags.FLAG_ATTRIBUTION_SOURCE_CONSTRUCTOR)
- public AttributionSource(int uid, int pid, @Nullable String packageName,
- @Nullable String attributionTag, @NonNull IBinder token,
- @Nullable String[] renouncedPermissions,
- @Nullable AttributionSource next) {
- this(uid, pid, packageName, attributionTag, token, renouncedPermissions,
- Context.DEVICE_ID_DEFAULT, next);
- }
-
- /** @hide */
- @TestApi
@FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED)
public AttributionSource(int uid, int pid, @Nullable String packageName,
@Nullable String attributionTag, @NonNull IBinder token,
diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java
index be40143..cd3ce87 100644
--- a/core/java/android/content/pm/ShortcutInfo.java
+++ b/core/java/android/content/pm/ShortcutInfo.java
@@ -1492,12 +1492,12 @@
/**
* Sets which surfaces a shortcut will be excluded from.
*
- * If the shortcut is set to be excluded from {@link #SURFACE_LAUNCHER}, shortcuts will be
- * excluded from the search result of {@link android.content.pm.LauncherApps#getShortcuts(
- * android.content.pm.LauncherApps.ShortcutQuery, UserHandle)} nor
- * {@link android.content.pm.ShortcutManager#getShortcuts(int)}. This generally means the
- * shortcut would not be displayed by a launcher app (e.g. in Long-Press menu), while
- * remain visible in other surfaces such as assistant or on-device-intelligence.
+ * This API is reserved for future extension. Currently, marking a shortcut to be
+ * excluded from {@link #SURFACE_LAUNCHER} will not publish the shortcut, thus
+ * the following operations will be a no-op:
+ * {@link android.content.pm.ShortcutManager#pushDynamicShortcut(android.content.pm.ShortcutInfo)},
+ * {@link android.content.pm.ShortcutManager#addDynamicShortcuts(List)}, and
+ * {@link android.content.pm.ShortcutManager#setDynamicShortcuts(List)}.
*/
@NonNull
public Builder setExcludedFromSurfaces(final int surfaces) {
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 45591d7..cee8d96 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -137,6 +137,18 @@
}
flag {
+ name: "get_package_storage_stats"
+ namespace: "system_performance"
+ is_exported: true
+ description: "Add dumpsys entry point for package StorageStats"
+ bug: "332905331"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "provide_info_of_apk_in_apex"
is_exported: true
namespace: "package_manager_service"
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java
index ac043d3..91b05c2 100644
--- a/core/java/android/hardware/hdmi/HdmiControlManager.java
+++ b/core/java/android/hardware/hdmi/HdmiControlManager.java
@@ -1353,9 +1353,6 @@
/**
* Get a snapshot of the real-time status of the devices on the CEC bus.
*
- * <p>This only applies to devices with switch functionality, which are devices with one
- * or more than one HDMI inputs.
- *
* @return a list of {@link HdmiDeviceInfo} of the connected CEC devices on the CEC bus. An
* empty list will be returned if there is none.
*/
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 243ae14..8f78032 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -148,8 +148,6 @@
IInputDeviceBatteryState getBatteryState(int deviceId);
- void setPointerIconType(int typeId);
- void setCustomPointerIcon(in PointerIcon icon);
boolean setPointerIcon(in PointerIcon icon, int displayId, int deviceId, int pointerId,
in IBinder inputToken);
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index dd4ea31..57004bc 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -992,21 +992,14 @@
}
/**
- * Changes the mouse pointer's icon shape into the specified id.
+ * This method exists for backwards-compatibility, and is a no-op.
*
- * @param iconId The id of the pointer graphic, as a value between
- * {@link PointerIcon#TYPE_ARROW} and {@link PointerIcon#TYPE_HANDWRITING}.
- *
+ * @deprecated
* @hide
*/
@UnsupportedAppUsage
public void setPointerIconType(int iconId) {
- mGlobal.setPointerIconType(iconId);
- }
-
- /** @hide */
- public void setCustomPointerIcon(PointerIcon icon) {
- mGlobal.setCustomPointerIcon(icon);
+ Log.e(TAG, "setPointerIcon: Unsupported app usage!");
}
/** @hide */
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index a9c97b1..cb3af2b 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -1411,28 +1411,6 @@
}
/**
- * @see InputManager#setPointerIconType(int)
- */
- public void setPointerIconType(int iconId) {
- try {
- mIm.setPointerIconType(iconId);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
- }
-
- /**
- * @see InputManager#setCustomPointerIcon(PointerIcon)
- */
- public void setCustomPointerIcon(PointerIcon icon) {
- try {
- mIm.setCustomPointerIcon(icon);
- } catch (RemoteException ex) {
- throw ex.rethrowFromSystemServer();
- }
- }
-
- /**
* @see InputManager#setPointerIcon(PointerIcon, int, int, int, IBinder)
*/
public boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId, int pointerId,
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index c6a9203..2f0d634 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -1930,12 +1930,10 @@
public static final String DISALLOW_THREAD_NETWORK = "no_thread_network";
/**
- * This user restriction specifies if the user is able to add SIMs to the device.
+ * This user restriction specifies if the user is able to add embedded SIMs to the device.
*
* <p>
- * This restriction blocks the download of embedded SIMs, and disables any physical SIMs.
- * If any embedded SIMs are already on the device, then they are removed. This restriction
- * does not affect SIMs provisioned to the device by device owners or profile owners.
+ * This restriction blocks the download of embedded SIMs.
*
* <p>
* This restriction can only be set by a device owner or a profile owner of an
@@ -1951,6 +1949,7 @@
*
* <p>Key for user restrictions.
* <p>Type: Boolean
+ *
* @see DevicePolicyManager#addUserRestriction(ComponentName, String)
* @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
* @see #getUserRestrictions()
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index b588308..0e28560 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -44,14 +44,6 @@
}
flag {
- name: "attribution_source_constructor"
- is_exported: true
- namespace: "permissions"
- description: "enable AttributionSource(int, int, String, String, IBinder, String[], AttributionSource)"
- bug: "304478648"
-}
-
-flag {
name: "enhanced_confirmation_mode_apis_enabled"
is_exported: true
is_fixed_read_only: true
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e6ddf35..009713f 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5123,13 +5123,6 @@
public static final String SCREEN_BRIGHTNESS = "screen_brightness";
/**
- * The screen backlight brightness between 0.0f and 1.0f.
- * @hide
- */
- @Readable
- public static final String SCREEN_BRIGHTNESS_FLOAT = "screen_brightness_float";
-
- /**
* Control whether to enable automatic brightness mode.
*/
@Readable
@@ -6273,7 +6266,6 @@
PUBLIC_SETTINGS.add(DIM_SCREEN);
PUBLIC_SETTINGS.add(SCREEN_OFF_TIMEOUT);
PUBLIC_SETTINGS.add(SCREEN_BRIGHTNESS);
- PUBLIC_SETTINGS.add(SCREEN_BRIGHTNESS_FLOAT);
PUBLIC_SETTINGS.add(SCREEN_BRIGHTNESS_MODE);
PUBLIC_SETTINGS.add(MODE_RINGER_STREAMS_AFFECTED);
PUBLIC_SETTINGS.add(MUTE_STREAMS_AFFECTED);
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index 51758aa..ee5e533 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -81,8 +81,15 @@
}
flag {
- name: "report_primary_auth_attempts"
- namespace: "biometrics"
- description: "Report primary auth attempts from LockSettingsService"
- bug: "285053096"
+ name: "report_primary_auth_attempts"
+ namespace: "biometrics"
+ description: "Report primary auth attempts from LockSettingsService"
+ bug: "285053096"
+}
+
+flag {
+ name: "dump_attestation_verifications"
+ namespace: "hardware_backed_security"
+ description: "Add a dump capability for attestation_verification service"
+ bug: "335498868"
}
diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl
index 1c0834f..3743035 100644
--- a/core/java/android/view/IWindow.aidl
+++ b/core/java/android/view/IWindow.aidl
@@ -108,11 +108,6 @@
void dispatchDragEvent(in DragEvent event);
/**
- * Pointer icon events
- */
- void updatePointerIcon(float x, float y);
-
- /**
* Called for non-application windows when the enter animation has completed.
*/
void dispatchWindowShown();
@@ -128,4 +123,9 @@
* @param callbacks to receive responses
*/
void requestScrollCapture(in IScrollCaptureResponseListener callbacks);
+
+ /**
+ * Dump the details of a window.
+ */
+ void dumpWindow(in ParcelFileDescriptor pfd);
}
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 86264eb..e3e4fc0 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -288,8 +288,6 @@
oneway void finishMovingTask(IWindow window);
- oneway void updatePointerIcon(IWindow window);
-
/**
* Update a tap exclude region identified by provided id in the window. Touches on this region
* will neither be dispatched to this window nor change the focus to this window. Passing an
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index a23df79..1d70d18 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -986,7 +986,7 @@
updateBackgroundVisibility(surfaceUpdateTransaction);
updateBackgroundColor(surfaceUpdateTransaction);
- if (mLimitedHdrEnabled && hdrHeadroomChanged) {
+ if (mLimitedHdrEnabled && (hdrHeadroomChanged || creating)) {
surfaceUpdateTransaction.setDesiredHdrHeadroom(
mBlastSurfaceControl, mHdrHeadroom);
}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 60ad926..1cb2765 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30695,21 +30695,11 @@
*/
public void setPointerIcon(PointerIcon pointerIcon) {
mMousePointerIcon = pointerIcon;
- if (com.android.input.flags.Flags.enablePointerChoreographer()) {
- final ViewRootImpl viewRootImpl = getViewRootImpl();
- if (viewRootImpl == null) {
- return;
- }
- viewRootImpl.refreshPointerIcon();
- } else {
- if (mAttachInfo == null || mAttachInfo.mHandlingPointerEvent) {
- return;
- }
- try {
- mAttachInfo.mSession.updatePointerIcon(mAttachInfo.mWindow);
- } catch (RemoteException e) {
- }
+ final ViewRootImpl viewRootImpl = getViewRootImpl();
+ if (viewRootImpl == null) {
+ return;
}
+ viewRootImpl.refreshPointerIcon();
}
/**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index fdf3cb1..155c053 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -74,7 +74,6 @@
import static android.view.ViewRootImplProto.WINDOW_ATTRIBUTES;
import static android.view.ViewRootImplProto.WIN_FRAME;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
-import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OVERRIDE_LAYOUT_IN_DISPLAY_CUTOUT_MODE;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS;
@@ -96,6 +95,7 @@
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OVERRIDE_LAYOUT_IN_DISPLAY_CUTOUT_MODE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
@@ -122,7 +122,6 @@
import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER;
import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;
-import static com.android.input.flags.Flags.enablePointerChoreographer;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.window.flags.Flags.activityWindowInfoFlag;
import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
@@ -195,6 +194,7 @@
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.StrictMode;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
@@ -268,11 +268,15 @@
import com.android.internal.os.IResultReceiver;
import com.android.internal.os.SomeArgs;
import com.android.internal.policy.PhoneFallbackEventHandler;
+import com.android.internal.util.FastPrintWriter;
import com.android.internal.view.BaseSurfaceHolder;
import com.android.internal.view.RootViewSurfaceTaker;
import com.android.internal.view.SurfaceCallbackHelper;
import com.android.modules.expresslog.Counter;
+import libcore.io.IoUtils;
+
+import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
@@ -423,6 +427,12 @@
private static final long NANOS_PER_SEC = 1000000000;
+ // If the ViewRootImpl has been idle for more than 200ms, clear the preferred
+ // frame rate category and frame rate.
+ private static final int IDLE_TIME_MILLIS = 250;
+
+ private static final long NANOS_PER_MILLI = 1_000_000;
+
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
static final ThreadLocal<HandlerActionQueue> sRunQueues = new ThreadLocal<HandlerActionQueue>();
@@ -655,6 +665,8 @@
private int mMinusOneFrameIntervalMillis = 0;
// VRR interval between the previous and the frame before
private int mMinusTwoFrameIntervalMillis = 0;
+ // VRR has the invalidation idle message been posted?
+ private boolean mInvalidationIdleMessagePosted = false;
/**
* Update the Choreographer's FrameInfo object with the timing information for the current
@@ -4266,6 +4278,10 @@
// when the values are applicable.
if (mDrawnThisFrame) {
mDrawnThisFrame = false;
+ if (!mInvalidationIdleMessagePosted) {
+ mInvalidationIdleMessagePosted = true;
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE, IDLE_TIME_MILLIS);
+ }
setCategoryFromCategoryCounts();
updateInfrequentCount();
setPreferredFrameRate(mPreferredFrameRate);
@@ -6507,6 +6523,8 @@
return "MSG_WINDOW_TOUCH_MODE_CHANGED";
case MSG_KEEP_CLEAR_RECTS_CHANGED:
return "MSG_KEEP_CLEAR_RECTS_CHANGED";
+ case MSG_CHECK_INVALIDATION_IDLE:
+ return "MSG_CHECK_INVALIDATION_IDLE";
case MSG_REFRESH_POINTER_ICON:
return "MSG_REFRESH_POINTER_ICON";
case MSG_TOUCH_BOOST_TIMEOUT:
@@ -6771,6 +6789,30 @@
mNumPausedForSync = 0;
scheduleTraversals();
break;
+ case MSG_CHECK_INVALIDATION_IDLE: {
+ long delta;
+ if (mIsTouchBoosting || mIsFrameRateBoosting || mInsetsAnimationRunning) {
+ delta = 0;
+ } else {
+ delta = System.nanoTime() / NANOS_PER_MILLI - mLastUpdateTimeMillis;
+ }
+ if (delta >= IDLE_TIME_MILLIS) {
+ mFrameRateCategoryHighCount = 0;
+ mFrameRateCategoryHighHintCount = 0;
+ mFrameRateCategoryNormalCount = 0;
+ mFrameRateCategoryLowCount = 0;
+ mPreferredFrameRate = 0;
+ mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
+ setPreferredFrameRateCategory(FRAME_RATE_CATEGORY_NO_PREFERENCE);
+ setPreferredFrameRate(0f);
+ mInvalidationIdleMessagePosted = false;
+ } else {
+ mInvalidationIdleMessagePosted = true;
+ mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE,
+ IDLE_TIME_MILLIS - delta);
+ }
+ break;
+ }
case MSG_TOUCH_BOOST_TIMEOUT:
/**
* Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME).
@@ -7937,46 +7979,20 @@
if (event.isStylusPointer() && mIsStylusPointerIconEnabled) {
pointerIcon = mHandwritingInitiator.onResolvePointerIcon(mContext, event);
}
-
if (pointerIcon == null) {
pointerIcon = mView.onResolvePointerIcon(event, pointerIndex);
}
-
- if (enablePointerChoreographer()) {
- if (pointerIcon == null) {
- pointerIcon = PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_NOT_SPECIFIED);
- }
- if (Objects.equals(mResolvedPointerIcon, pointerIcon)) {
- return true;
- }
- mResolvedPointerIcon = pointerIcon;
-
- InputManagerGlobal.getInstance()
- .setPointerIcon(pointerIcon, event.getDisplayId(),
- event.getDeviceId(), event.getPointerId(0), getInputToken());
+ if (pointerIcon == null) {
+ pointerIcon = PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_NOT_SPECIFIED);
+ }
+ if (Objects.equals(mResolvedPointerIcon, pointerIcon)) {
return true;
}
+ mResolvedPointerIcon = pointerIcon;
- final int pointerType = (pointerIcon != null) ?
- pointerIcon.getType() : PointerIcon.TYPE_NOT_SPECIFIED;
-
- if (mPointerIconType == null || mPointerIconType != pointerType) {
- mPointerIconType = pointerType;
- mCustomPointerIcon = null;
- if (mPointerIconType != PointerIcon.TYPE_CUSTOM) {
- InputManagerGlobal
- .getInstance()
- .setPointerIconType(pointerType);
- return true;
- }
- }
- if (mPointerIconType == PointerIcon.TYPE_CUSTOM &&
- !pointerIcon.equals(mCustomPointerIcon)) {
- mCustomPointerIcon = pointerIcon;
- InputManagerGlobal
- .getInstance()
- .setCustomPointerIcon(mCustomPointerIcon);
- }
+ InputManagerGlobal.getInstance()
+ .setPointerIcon(pointerIcon, event.getDisplayId(),
+ event.getDeviceId(), event.getPointerId(0), getInputToken());
return true;
}
@@ -10580,16 +10596,6 @@
mHandler.sendMessage(msg);
}
- public void updatePointerIcon(float x, float y) {
- final int what = MSG_UPDATE_POINTER_ICON;
- mHandler.removeMessages(what);
- final long now = SystemClock.uptimeMillis();
- final MotionEvent event = MotionEvent.obtain(
- 0, now, MotionEvent.ACTION_HOVER_MOVE, x, y, 0);
- Message msg = mHandler.obtainMessage(what, event);
- mHandler.sendMessage(msg);
- }
-
public void dispatchCheckFocus() {
if (!mHandler.hasMessages(MSG_CHECK_FOCUS)) {
// This will result in a call to checkFocus() below.
@@ -11453,14 +11459,6 @@
}
@Override
- public void updatePointerIcon(float x, float y) {
- final ViewRootImpl viewAncestor = mViewAncestor.get();
- if (viewAncestor != null) {
- viewAncestor.updatePointerIcon(x, y);
- }
- }
-
- @Override
public void dispatchWindowShown() {
final ViewRootImpl viewAncestor = mViewAncestor.get();
if (viewAncestor != null) {
@@ -11483,6 +11481,26 @@
viewAncestor.dispatchScrollCaptureRequest(listener);
}
}
+
+ @Override
+ public void dumpWindow(ParcelFileDescriptor pfd) {
+ final ViewRootImpl viewAncestor = mViewAncestor.get();
+ if (viewAncestor == null) {
+ return;
+ }
+ viewAncestor.mHandler.postAtFrontOfQueue(() -> {
+ final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
+ try {
+ PrintWriter pw = new FastPrintWriter(new FileOutputStream(
+ pfd.getFileDescriptor()));
+ viewAncestor.dump("", pw);
+ pw.flush();
+ } finally {
+ IoUtils.closeQuietly(pfd);
+ StrictMode.setThreadPolicy(oldPolicy);
+ }
+ });
+ }
}
public static final class CalledFromWrongThreadException extends AndroidRuntimeException {
@@ -13016,6 +13034,10 @@
private void removeVrrMessages() {
mHandler.removeMessages(MSG_TOUCH_BOOST_TIMEOUT);
mHandler.removeMessages(MSG_FRAME_RATE_SETTING);
+ if (mInvalidationIdleMessagePosted) {
+ mInvalidationIdleMessagePosted = false;
+ mHandler.removeMessages(MSG_CHECK_INVALIDATION_IDLE);
+ }
}
/**
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index e6367ff..d7d764b 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -603,10 +603,6 @@
}
@Override
- public void updatePointerIcon(android.view.IWindow window) {
- }
-
- @Override
public void updateTapExcludeRegion(android.view.IWindow window,
android.graphics.Region region) {
}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index a073873..cf128fb 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -2530,18 +2530,6 @@
view, /* delegatorPackageName= */ null, /* handwritingDelegateFlags= */ 0);
}
- private void startStylusHandwritingInternalAsync(
- @NonNull View view, @Nullable String delegatorPackageName,
- @HandwritingDelegateFlags int handwritingDelegateFlags,
- @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Boolean> callback) {
- Objects.requireNonNull(view);
- Objects.requireNonNull(executor);
- Objects.requireNonNull(callback);
-
- startStylusHandwritingInternal(
- view, delegatorPackageName, handwritingDelegateFlags, executor, callback);
- }
-
private void sendFailureCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<Boolean> callback) {
if (executor == null || callback == null) {
@@ -2891,7 +2879,7 @@
if (Flags.homeScreenHandwritingDelegator()) {
flags = delegateView.getHandwritingDelegateFlags();
}
- startStylusHandwritingInternalAsync(
+ acceptStylusHandwritingDelegation(
delegateView, delegatorPackageName, flags, executor, callback);
}
@@ -2926,6 +2914,9 @@
@HandwritingDelegateFlags int flags, @NonNull @CallbackExecutor Executor executor,
@NonNull Consumer<Boolean> callback) {
Objects.requireNonNull(delegatorPackageName);
+ Objects.requireNonNull(delegateView);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
startStylusHandwritingInternal(
delegateView, delegatorPackageName, flags, executor, callback);
diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java
index a07141b..b7ee0b8 100644
--- a/core/java/android/webkit/WebChromeClient.java
+++ b/core/java/android/webkit/WebChromeClient.java
@@ -520,6 +520,13 @@
* To cancel the request, call <code>filePathCallback.onReceiveValue(null)</code> and
* return {@code true}.
*
+ * <p class="note"><b>Note:</b> WebView does not enforce any restrictions on
+ * the chosen file(s). WebView can access all files that your app can access.
+ * In case the file(s) are chosen through an untrusted source such as a third-party
+ * app, it is your own app's responsibility to check what the returned Uris
+ * refer to before calling the <code>filePathCallback</code>. See
+ * {@link #createIntent} and {@link #parseResult} for more details.</p>
+ *
* @param webView The WebView instance that is initiating the request.
* @param filePathCallback Invoke this callback to supply the list of paths to files to upload,
* or {@code null} to cancel. Must only be called if the
@@ -556,6 +563,15 @@
* Parse the result returned by the file picker activity. This method should be used with
* {@link #createIntent}. Refer to {@link #createIntent} for how to use it.
*
+ * <p class="note"><b>Note:</b> The intent returned by the file picker activity
+ * should be treated as untrusted. A third-party app handling the implicit
+ * intent created by {@link #createIntent} might return Uris that the third-party
+ * app itself does not have access to, such as your own app's sensitive data files.
+ * WebView does not enforce any restrictions on the returned Uris. It is the
+ * app's responsibility to ensure that the untrusted source (such as a third-party
+ * app) has access the Uris it has returned and that the Uris are not pointing
+ * to any sensitive data files.</p>
+ *
* @param resultCode the integer result code returned by the file picker activity.
* @param data the intent returned by the file picker activity.
* @return the Uris of selected file(s) or {@code null} if the resultCode indicates
@@ -618,6 +634,12 @@
* WebChromeClient#onShowFileChooser}</li>
* </ol>
*
+ * <p class="note"><b>Note:</b> The created intent may be handled by
+ * third-party applications on device. The received result must be treated
+ * as untrusted as it can contain Uris pointing to your own app's sensitive
+ * data files. Your app should check the resultant Uris in {@link #parseResult}
+ * before calling the <code>filePathCallback</code>.</p>
+ *
* @return an Intent that supports basic file chooser sources.
*/
public abstract Intent createIntent();
diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig
index 33af486..69cac6f 100644
--- a/core/java/android/window/flags/responsible_apis.aconfig
+++ b/core/java/android/window/flags/responsible_apis.aconfig
@@ -49,3 +49,10 @@
description: "Prevent BAL based on it is bound by foreground Uid but the app switch is stopped."
bug: "283801068"
}
+
+flag {
+ name: "bal_improved_metrics"
+ namespace: "responsible_apis"
+ description: "Improved metrics."
+ bug: "339245692"
+}
diff --git a/core/java/com/android/internal/util/NewlineNormalizer.java b/core/java/com/android/internal/util/NewlineNormalizer.java
new file mode 100644
index 0000000..0104d1f
--- /dev/null
+++ b/core/java/com/android/internal/util/NewlineNormalizer.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+
+import java.util.regex.Pattern;
+
+/**
+ * Utility class that replaces consecutive empty lines with single new line.
+ * @hide
+ */
+public class NewlineNormalizer {
+
+ private static final Pattern MULTIPLE_NEWLINES = Pattern.compile("\\v(\\s*\\v)?");
+
+ // Private constructor to prevent instantiation
+ private NewlineNormalizer() {}
+
+ /**
+ * Replaces consecutive newlines with a single newline in the input text.
+ */
+ public static String normalizeNewlines(String text) {
+ return MULTIPLE_NEWLINES.matcher(text).replaceAll("\n");
+ }
+}
diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java
index e33704b..3fc4fff 100644
--- a/core/java/com/android/internal/view/BaseIWindow.java
+++ b/core/java/com/android/internal/view/BaseIWindow.java
@@ -18,7 +18,6 @@
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
-import android.hardware.input.InputManagerGlobal;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
@@ -29,7 +28,6 @@
import android.view.IWindowSession;
import android.view.InsetsSourceControl;
import android.view.InsetsState;
-import android.view.PointerIcon;
import android.view.ScrollCaptureResponse;
import android.view.WindowInsets.Type.InsetsType;
import android.view.inputmethod.ImeTracker;
@@ -128,12 +126,6 @@
}
@Override
- public void updatePointerIcon(float x, float y) {
- InputManagerGlobal.getInstance()
- .setPointerIconType(PointerIcon.TYPE_NOT_SPECIFIED);
- }
-
- @Override
public void dispatchWallpaperCommand(String action, int x, int y,
int z, Bundle extras, boolean sync) {
if (sync) {
@@ -162,4 +154,9 @@
// ignore
}
}
+
+ @Override
+ public void dumpWindow(ParcelFileDescriptor pfd) {
+
+ }
}
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 12d62cc..062fab3 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -116,7 +116,7 @@
using android::zygote::ZygoteFailure;
-using Action = android_mallopt_gwp_asan_options_t::Action;
+using Mode = android_mallopt_gwp_asan_options_t::Mode;
// This type is duplicated in fd_utils.h
typedef const std::function<void(std::string)>& fail_fn_t;
@@ -2101,21 +2101,21 @@
switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) {
default:
case RuntimeFlags::GWP_ASAN_LEVEL_DEFAULT:
- gwp_asan_options.desire = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true)
- ? Action::TURN_ON_FOR_APP_SAMPLED_NON_CRASHING
- : Action::DONT_TURN_ON_UNLESS_OVERRIDDEN;
+ gwp_asan_options.mode = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true)
+ ? Mode::APP_MANIFEST_DEFAULT
+ : Mode::APP_MANIFEST_NEVER;
android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
break;
case RuntimeFlags::GWP_ASAN_LEVEL_NEVER:
- gwp_asan_options.desire = Action::DONT_TURN_ON_UNLESS_OVERRIDDEN;
+ gwp_asan_options.mode = Mode::APP_MANIFEST_NEVER;
android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
break;
case RuntimeFlags::GWP_ASAN_LEVEL_ALWAYS:
- gwp_asan_options.desire = Action::TURN_ON_FOR_APP;
+ gwp_asan_options.mode = Mode::APP_MANIFEST_ALWAYS;
android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
break;
case RuntimeFlags::GWP_ASAN_LEVEL_LOTTERY:
- gwp_asan_options.desire = Action::TURN_ON_WITH_SAMPLING;
+ gwp_asan_options.mode = Mode::APP_MANIFEST_DEFAULT;
android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options));
break;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index bfbfb3a..70d923b 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8771,6 +8771,7 @@
<service android:name="com.android.server.companion.datatransfer.contextsync.CallMetadataSyncInCallService"
android:permission="android.permission.BIND_INCALL_SERVICE"
+ android:enabled="@bool/config_enableContextSyncInCall"
android:exported="true">
<meta-data android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
android:value="true" />
diff --git a/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml
index c0fe536..7c45c20 100644
--- a/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_1_4_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 11 13 L 2 22 L 11 22 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 1 bar. move to higher ground. -->
+ <path
+ android:name="ic_signal_cellular_1_4_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H11 V20 H6 z" />
+ </clip-path>
</vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml
index 816da22..02b646d 100644
--- a/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_1_5_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M8.72,15.28,2,22H8.72V15.28Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 1 bar. might have to call you back. -->
+ <path
+ android:name="ic_signal_cellular_1_5_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H12 V20 H6 z" />
+ </clip-path>
</vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml
index 69a966b..514d169 100644
--- a/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_2_4_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 13 11 L 2 22 L 13 22 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 2 bars. 2 out of 4 ain't bad. -->
+ <path
+ android:name="ic_signal_cellular_2_4_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H14 V20 H6 z" />
+ </clip-path>
</vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml
index 02c7a43..a97f771 100644
--- a/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_2_5_bar.xml
@@ -23,7 +23,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 11.45 12.55 L 2 22 L 11.45 22 L 11.45 12.55 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 2 bars. hanging in there. -->
+ <path
+ android:name="ic_signal_cellular_2_5_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H14 V20 H6 z" />
+ </clip-path>
</vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml b/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml
index 46ce47c..1bacf4a 100644
--- a/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_3_4_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7v13H7L20,7 M22,2L2,22h20V2L22,2z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 2 22 L 16 22 L 16 21 L 16 20 L 16 11 L 16 10 L 16 8 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 3 bars. quite nice. -->
+ <path
+ android:name="ic_signal_cellular_3_4_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H17 V20 H6 z" />
+ </clip-path>
</vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml
index 37435e6b..2789d3e 100644
--- a/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_3_5_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 14.96 9.04 L 2 22 L 14.96 22 L 14.96 9.04 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 3 bars. not great, not terrible. -->
+ <path
+ android:name="ic_signal_cellular_3_5_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H16 V20 H6 z" />
+ </clip-path>
</vector>
\ No newline at end of file
diff --git a/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml b/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml
index 6dc3646..8286dbb 100644
--- a/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml
+++ b/core/res/res/drawable/ic_signal_cellular_4_5_bar.xml
@@ -22,7 +22,11 @@
<path
android:fillColor="@android:color/white"
android:pathData="M20,7V20H7L20,7m2-5L2,22H22V2Z" />
- <path
- android:fillColor="@android:color/white"
- android:pathData="M 18.48 5.52 L 2 22 L 18.48 22 L 18.48 5.52 Z" />
+ <clip-path android:name="triangle" android:pathData="M20,7v13H7L20,7z">
+ <!-- 4 bars. extremely respectable. -->
+ <path
+ android:name="ic_signal_cellular_4_5_bar"
+ android:fillColor="@android:color/white"
+ android:pathData="M6,0 H18 V20 H6 z" />
+ </clip-path>
</vector>
\ No newline at end of file
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index edaf8b5..cefc648 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -7051,6 +7051,9 @@
event gets ignored. -->
<integer name="config_defaultMinEmergencyGestureTapDurationMillis">200</integer>
+ <!-- Control whether to enable CallMetadataSyncInCallService. -->
+ <bool name="config_enableContextSyncInCall">false</bool>
+
<!-- Whether the system uses auto-suspend mode. -->
<bool name="config_useAutoSuspend">true</bool>
</resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 1fca4f8..59e4161 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6484,4 +6484,23 @@
<string name="satellite_notification_how_it_works">How it works</string>
<!-- Initial/System provided label shown for an app which gets unarchived. [CHAR LIMIT=64]. -->
<string name="unarchival_session_app_label">Pending...</string>
+
+ <!-- Fingerprint dangling notification title -->
+ <string name="fingerprint_dangling_notification_title">Set up Fingerprint Unlock again</string>
+ <!-- Fingerprint dangling notification content for only 1 fingerprint deleted -->
+ <string name="fingerprint_dangling_notification_msg_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted to improve performance</string>
+ <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted -->
+ <string name="fingerprint_dangling_notification_msg_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted to improve performance</string>
+ <!-- Fingerprint dangling notification content for only 1 fingerprint deleted and no fingerprint left-->
+ <string name="fingerprint_dangling_notification_msg_all_deleted_1"><xliff:g id="fingerprint">%s</xliff:g> wasn\'t working well and was deleted. Set it up again to unlock your phone with fingerprint.</string>
+ <!-- Fingerprint dangling notification content for more than 1 fingerprints deleted and no fingerprint left -->
+ <string name="fingerprint_dangling_notification_msg_all_deleted_2"><xliff:g id="fingerprint">%1$s</xliff:g> and <xliff:g id="fingerprint">%2$s</xliff:g> weren\'t working well and were deleted. Set them up again to unlock your phone with your fingerprint.</string>
+ <!-- Face dangling notification title -->
+ <string name="face_dangling_notification_title">Set up Face Unlock again</string>
+ <!-- Face dangling notification content -->
+ <string name="face_dangling_notification_msg">Your face model wasn\'t working well and was deleted. Set it up again to unlock your phone with face.</string>
+ <!-- Biometric dangling notification "set up" action button -->
+ <string name="biometric_dangling_notification_action_set_up">Set up</string>
+ <!-- Biometric dangling notification "Not now" action button -->
+ <string name="biometric_dangling_notification_action_not_now">Not now</string>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 36542ee..d058fb1 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5432,4 +5432,15 @@
<!-- For PowerManagerService to determine whether to use auto-suspend mode -->
<java-symbol type="bool" name="config_useAutoSuspend" />
+
+ <!-- Biometric dangling notification strings -->
+ <java-symbol type="string" name="fingerprint_dangling_notification_title" />
+ <java-symbol type="string" name="fingerprint_dangling_notification_msg_1" />
+ <java-symbol type="string" name="fingerprint_dangling_notification_msg_2" />
+ <java-symbol type="string" name="fingerprint_dangling_notification_msg_all_deleted_1" />
+ <java-symbol type="string" name="fingerprint_dangling_notification_msg_all_deleted_2" />
+ <java-symbol type="string" name="face_dangling_notification_title" />
+ <java-symbol type="string" name="face_dangling_notification_msg" />
+ <java-symbol type="string" name="biometric_dangling_notification_action_set_up" />
+ <java-symbol type="string" name="biometric_dangling_notification_action_not_now" />
</resources>
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index 0b1b40c..bc0ae9f 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -623,6 +623,28 @@
assertEquals(FRAME_RATE_CATEGORY_HIGH_HINT, mViewRoot.getLastPreferredFrameRateCategory());
}
+ @Test
+ @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY
+ })
+ public void idleDetected() throws Throwable {
+ waitForFrameRateCategoryToSettle();
+ mActivityRule.runOnUiThread(() -> {
+ mMovingView.setRequestedFrameRate(View.REQUESTED_FRAME_RATE_CATEGORY_HIGH);
+ mMovingView.setFrameContentVelocity(Float.MAX_VALUE);
+ mMovingView.invalidate();
+ runAfterDraw(() -> assertEquals(FRAME_RATE_CATEGORY_HIGH,
+ mViewRoot.getLastPreferredFrameRateCategory()));
+ });
+ waitForAfterDraw();
+
+ // Wait for idle timeout
+ Thread.sleep(500);
+ assertEquals(0f, mViewRoot.getLastPreferredFrameRate());
+ assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE,
+ mViewRoot.getLastPreferredFrameRateCategory());
+ }
+
private void runAfterDraw(@NonNull Runnable runnable) {
Handler handler = new Handler(Looper.getMainLooper());
mAfterDrawLatch = new CountDownLatch(1);
diff --git a/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java b/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java
new file mode 100644
index 0000000..bcdac61
--- /dev/null
+++ b/core/tests/utiltests/src/com/android/internal/util/NewlineNormalizerTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.util;
+
+import static junit.framework.Assert.assertEquals;
+
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for {@link NewlineNormalizer}
+ * @hide
+ */
+@DisabledOnRavenwood(blockedBy = NewlineNormalizer.class)
+@RunWith(AndroidJUnit4.class)
+public class NewlineNormalizerTest {
+
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+ @Test
+ public void testEmptyInput() {
+ assertEquals("", NewlineNormalizer.normalizeNewlines(""));
+ }
+
+ @Test
+ public void testSingleNewline() {
+ assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n"));
+ }
+
+ @Test
+ public void testMultipleConsecutiveNewlines() {
+ assertEquals("\n", NewlineNormalizer.normalizeNewlines("\n\n\n\n\n"));
+ }
+
+ @Test
+ public void testNewlinesWithSpacesAndTabs() {
+ String input = "Line 1\n \n \t \n\tLine 2";
+ // Adjusted expected output to include the tab character
+ String expected = "Line 1\n\tLine 2";
+ assertEquals(expected, NewlineNormalizer.normalizeNewlines(input));
+ }
+
+ @Test
+ public void testMixedNewlineCharacters() {
+ String input = "Line 1\r\nLine 2\u000BLine 3\fLine 4\u2028Line 5\u2029Line 6";
+ String expected = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6";
+ assertEquals(expected, NewlineNormalizer.normalizeNewlines(input));
+ }
+}
diff --git a/graphics/java/android/graphics/RecordingCanvas.java b/graphics/java/android/graphics/RecordingCanvas.java
index 635e78e..cc5b3b9 100644
--- a/graphics/java/android/graphics/RecordingCanvas.java
+++ b/graphics/java/android/graphics/RecordingCanvas.java
@@ -40,7 +40,7 @@
/** @hide */
private static int getPanelFrameSize() {
- final int DefaultSize = 100 * 1024 * 1024; // 100 MB;
+ final int DefaultSize = 150 * 1024 * 1024; // 150 MB;
return Math.max(SystemProperties.getInt("ro.hwui.max_texture_allocation_size", DefaultSize),
DefaultSize);
}
@@ -262,7 +262,7 @@
protected void throwIfCannotDraw(Bitmap bitmap) {
super.throwIfCannotDraw(bitmap);
int bitmapSize = bitmap.getByteCount();
- if (bitmapSize > MAX_BITMAP_SIZE) {
+ if (bitmap.getConfig() != Bitmap.Config.HARDWARE && bitmapSize > MAX_BITMAP_SIZE) {
throw new RuntimeException(
"Canvas: trying to draw too large(" + bitmapSize + "bytes) bitmap.");
}
diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java
deleted file mode 100644
index d1d7c14..0000000
--- a/keystore/java/android/security/KeyStore.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2009 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.security;
-
-/**
- * This class provides some constants and helper methods related to Android's Keystore service.
- * This class was originally much larger, but its functionality was superseded by other classes.
- * It now just contains a few remaining pieces for which the users haven't been updated yet.
- * You may be looking for {@link java.security.KeyStore} instead.
- *
- * @hide
- */
-public class KeyStore {
-
- // Used for UID field to indicate the calling UID.
- public static final int UID_SELF = -1;
-}
diff --git a/ktfmt_includes.txt b/ktfmt_includes.txt
index fe47503..0ac6265 100644
--- a/ktfmt_includes.txt
+++ b/ktfmt_includes.txt
@@ -5,8 +5,6 @@
-packages/SystemUI/checks/src/com/android/internal/systemui/lint/BroadcastSentViaContextDetector.kt
-packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterReceiverViaContextDetector.kt
-packages/SystemUI/checks/src/com/android/internal/systemui/lint/SoftwareBitmapDetector.kt
--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
-packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt
-packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/View.kt
-packages/SystemUI/shared/src/com/android/systemui/flags/Flag.kt
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java
index d923a46..d241641 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java
@@ -16,6 +16,8 @@
package androidx.window.common;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
@@ -26,30 +28,30 @@
*/
public class EmptyLifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks {
@Override
- public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+ public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
}
@Override
- public void onActivityStarted(Activity activity) {
+ public void onActivityStarted(@NonNull Activity activity) {
}
@Override
- public void onActivityResumed(Activity activity) {
+ public void onActivityResumed(@NonNull Activity activity) {
}
@Override
- public void onActivityPaused(Activity activity) {
+ public void onActivityPaused(@NonNull Activity activity) {
}
@Override
- public void onActivityStopped(Activity activity) {
+ public void onActivityStopped(@NonNull Activity activity) {
}
@Override
- public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+ public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
- public void onActivityDestroyed(Activity activity) {
+ public void onActivityDestroyed(@NonNull Activity activity) {
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index a0d6fce..6793fa5 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -35,6 +35,7 @@
import android.annotation.DimenRes;
import android.annotation.Nullable;
+import android.app.Activity;
import android.app.ActivityThread;
import android.content.Context;
import android.content.pm.ActivityInfo;
@@ -42,6 +43,7 @@
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RotateDrawable;
import android.hardware.display.DisplayManager;
@@ -213,7 +215,11 @@
isVerticalSplit,
isReversedLayout,
parentInfo.getDisplayId(),
- isDraggableExpandType
+ isDraggableExpandType,
+ getContainerBackgroundColor(topSplitContainer.getPrimaryContainer(),
+ DEFAULT_PRIMARY_VEIL_COLOR),
+ getContainerBackgroundColor(topSplitContainer.getSecondaryContainer(),
+ DEFAULT_SECONDARY_VEIL_COLOR)
));
}
}
@@ -242,6 +248,27 @@
}
/**
+ * Returns the window background color of the top activity in the container if set, or the
+ * default color if the background color of the top activity is unavailable.
+ */
+ @VisibleForTesting
+ @NonNull
+ static Color getContainerBackgroundColor(
+ @NonNull TaskFragmentContainer container, @NonNull Color defaultColor) {
+ final Activity activity = container.getTopNonFinishingActivity();
+ if (activity == null || !activity.isResumed()) {
+ // This can happen when the top activity in the container is from a different process.
+ return defaultColor;
+ }
+
+ final Drawable drawable = activity.getWindow().getDecorView().getBackground();
+ if (drawable instanceof ColorDrawable colorDrawable) {
+ return Color.valueOf(colorDrawable.getColor());
+ }
+ return defaultColor;
+ }
+
+ /**
* Creates a decor surface for the TaskFragment if no decor surface exists, or changes the owner
* of the existing decor surface to be the specified TaskFragment.
*
@@ -800,6 +827,8 @@
private final int mDisplayId;
private final boolean mIsReversedLayout;
private final boolean mIsDraggableExpandType;
+ private final Color mPrimaryVeilColor;
+ private final Color mSecondaryVeilColor;
@VisibleForTesting
Properties(
@@ -810,7 +839,9 @@
boolean isVerticalSplit,
boolean isReversedLayout,
int displayId,
- boolean isDraggableExpandType) {
+ boolean isDraggableExpandType,
+ @NonNull Color primaryVeilColor,
+ @NonNull Color secondaryVeilColor) {
mConfiguration = configuration;
mDividerAttributes = dividerAttributes;
mDecorSurface = decorSurface;
@@ -819,6 +850,8 @@
mIsReversedLayout = isReversedLayout;
mDisplayId = displayId;
mIsDraggableExpandType = isDraggableExpandType;
+ mPrimaryVeilColor = primaryVeilColor;
+ mSecondaryVeilColor = secondaryVeilColor;
}
/**
@@ -840,7 +873,9 @@
&& a.mIsVerticalSplit == b.mIsVerticalSplit
&& a.mDisplayId == b.mDisplayId
&& a.mIsReversedLayout == b.mIsReversedLayout
- && a.mIsDraggableExpandType == b.mIsDraggableExpandType;
+ && a.mIsDraggableExpandType == b.mIsDraggableExpandType
+ && a.mPrimaryVeilColor.equals(b.mPrimaryVeilColor)
+ && a.mSecondaryVeilColor.equals(b.mSecondaryVeilColor);
}
private static boolean areSameSurfaces(
@@ -1087,8 +1122,8 @@
}
private void showVeils(@NonNull SurfaceControl.Transaction t) {
- t.setColor(mPrimaryVeil, colorToFloatArray(DEFAULT_PRIMARY_VEIL_COLOR))
- .setColor(mSecondaryVeil, colorToFloatArray(DEFAULT_SECONDARY_VEIL_COLOR))
+ t.setColor(mPrimaryVeil, colorToFloatArray(mProperties.mPrimaryVeilColor))
+ .setColor(mSecondaryVeil, colorToFloatArray(mProperties.mSecondaryVeilColor))
.setLayer(mDividerSurface, DIVIDER_LAYER)
.setLayer(mPrimaryVeil, VEIL_LAYER)
.setLayer(mSecondaryVeil, VEIL_LAYER)
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
index 56c3bce..339908a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
@@ -16,16 +16,10 @@
package androidx.window.sidecar;
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
-import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
-
+import android.annotation.Nullable;
import android.app.Activity;
-import android.app.ActivityThread;
import android.app.Application;
import android.content.Context;
-import android.graphics.Rect;
import android.hardware.devicestate.DeviceStateManager;
import android.os.Bundle;
import android.os.IBinder;
@@ -38,7 +32,6 @@
import androidx.window.util.BaseDataProducer;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
/**
@@ -76,64 +69,13 @@
@NonNull
@Override
public SidecarDeviceState getDeviceState() {
- SidecarDeviceState deviceState = new SidecarDeviceState();
- deviceState.posture = deviceStateFromFeature();
- return deviceState;
- }
-
- private int deviceStateFromFeature() {
- for (int i = 0; i < mStoredFeatures.size(); i++) {
- CommonFoldingFeature feature = mStoredFeatures.get(i);
- final int state = feature.getState();
- switch (state) {
- case CommonFoldingFeature.COMMON_STATE_FLAT:
- return SidecarDeviceState.POSTURE_OPENED;
- case CommonFoldingFeature.COMMON_STATE_HALF_OPENED:
- return SidecarDeviceState.POSTURE_HALF_OPENED;
- case CommonFoldingFeature.COMMON_STATE_UNKNOWN:
- return SidecarDeviceState.POSTURE_UNKNOWN;
- }
- }
- return SidecarDeviceState.POSTURE_UNKNOWN;
+ return SidecarHelper.calculateDeviceState(mStoredFeatures);
}
@NonNull
@Override
public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) {
- Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken);
- SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo();
- if (activity == null) {
- return windowLayoutInfo;
- }
- windowLayoutInfo.displayFeatures = getDisplayFeatures(activity);
- return windowLayoutInfo;
- }
-
- private List<SidecarDisplayFeature> getDisplayFeatures(@NonNull Activity activity) {
- int displayId = activity.getDisplay().getDisplayId();
- if (displayId != DEFAULT_DISPLAY) {
- return Collections.emptyList();
- }
-
- if (activity.isInMultiWindowMode()) {
- // It is recommended not to report any display features in multi-window mode, since it
- // won't be possible to synchronize the display feature positions with window movement.
- return Collections.emptyList();
- }
-
- List<SidecarDisplayFeature> features = new ArrayList<>();
- final int rotation = activity.getResources().getConfiguration().windowConfiguration
- .getDisplayRotation();
- for (CommonFoldingFeature baseFeature : mStoredFeatures) {
- SidecarDisplayFeature feature = new SidecarDisplayFeature();
- Rect featureRect = baseFeature.getRect();
- rotateRectToDisplayRotation(displayId, rotation, featureRect);
- transformToWindowSpaceRect(activity, featureRect);
- feature.setRect(featureRect);
- feature.setType(baseFeature.getType());
- features.add(feature);
- }
- return Collections.unmodifiableList(features);
+ return SidecarHelper.calculateWindowLayoutInfo(windowToken, mStoredFeatures);
}
@Override
@@ -145,13 +87,14 @@
private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter {
@Override
- public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+ public void onActivityCreated(@NonNull Activity activity,
+ @Nullable Bundle savedInstanceState) {
super.onActivityCreated(activity, savedInstanceState);
onDisplayFeaturesChangedForActivity(activity);
}
@Override
- public void onActivityConfigurationChanged(Activity activity) {
+ public void onActivityConfigurationChanged(@NonNull Activity activity) {
super.onActivityConfigurationChanged(activity);
onDisplayFeaturesChangedForActivity(activity);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
new file mode 100644
index 0000000..bb6ab47
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
@@ -0,0 +1,129 @@
+/*
+ * 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 androidx.window.sidecar;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
+import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.ActivityThread;
+import android.graphics.Rect;
+import android.os.IBinder;
+
+import androidx.window.common.CommonFoldingFeature;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A utility class for transforming between Sidecar and Extensions features.
+ */
+class SidecarHelper {
+
+ private SidecarHelper() {}
+
+ /**
+ * Returns the {@link SidecarDeviceState} posture that is calculated for the first fold in
+ * the feature list. Sidecar devices only have one fold so we only pick the first one to
+ * determine the state.
+ * @param featureList the {@link CommonFoldingFeature} that are currently active.
+ * @return the {@link SidecarDeviceState} calculated from the {@link List} of
+ * {@link CommonFoldingFeature}.
+ */
+ @SuppressWarnings("deprecation")
+ private static int deviceStateFromFeatureList(@NonNull List<CommonFoldingFeature> featureList) {
+ for (int i = 0; i < featureList.size(); i++) {
+ final CommonFoldingFeature feature = featureList.get(i);
+ final int state = feature.getState();
+ switch (state) {
+ case CommonFoldingFeature.COMMON_STATE_FLAT:
+ return SidecarDeviceState.POSTURE_OPENED;
+ case CommonFoldingFeature.COMMON_STATE_HALF_OPENED:
+ return SidecarDeviceState.POSTURE_HALF_OPENED;
+ case CommonFoldingFeature.COMMON_STATE_UNKNOWN:
+ return SidecarDeviceState.POSTURE_UNKNOWN;
+ case CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES:
+ return SidecarDeviceState.POSTURE_UNKNOWN;
+ case CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE:
+ return SidecarDeviceState.POSTURE_UNKNOWN;
+ }
+ }
+ return SidecarDeviceState.POSTURE_UNKNOWN;
+ }
+
+ /**
+ * Returns a {@link SidecarDeviceState} calculated from a {@link List} of
+ * {@link CommonFoldingFeature}s.
+ */
+ @SuppressWarnings("deprecation")
+ static SidecarDeviceState calculateDeviceState(
+ @NonNull List<CommonFoldingFeature> featureList) {
+ final SidecarDeviceState deviceState = new SidecarDeviceState();
+ deviceState.posture = deviceStateFromFeatureList(featureList);
+ return deviceState;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static List<SidecarDisplayFeature> calculateDisplayFeatures(
+ @NonNull Activity activity,
+ @NonNull List<CommonFoldingFeature> featureList
+ ) {
+ final int displayId = activity.getDisplay().getDisplayId();
+ if (displayId != DEFAULT_DISPLAY) {
+ return Collections.emptyList();
+ }
+
+ if (activity.isInMultiWindowMode()) {
+ // It is recommended not to report any display features in multi-window mode, since it
+ // won't be possible to synchronize the display feature positions with window movement.
+ return Collections.emptyList();
+ }
+
+ final List<SidecarDisplayFeature> features = new ArrayList<>();
+ final int rotation = activity.getResources().getConfiguration().windowConfiguration
+ .getDisplayRotation();
+ for (CommonFoldingFeature baseFeature : featureList) {
+ final SidecarDisplayFeature feature = new SidecarDisplayFeature();
+ final Rect featureRect = baseFeature.getRect();
+ rotateRectToDisplayRotation(displayId, rotation, featureRect);
+ transformToWindowSpaceRect(activity, featureRect);
+ feature.setRect(featureRect);
+ feature.setType(baseFeature.getType());
+ features.add(feature);
+ }
+ return Collections.unmodifiableList(features);
+ }
+
+ /**
+ * Returns a {@link SidecarWindowLayoutInfo} calculated from the {@link List} of
+ * {@link CommonFoldingFeature}.
+ */
+ @SuppressWarnings("deprecation")
+ static SidecarWindowLayoutInfo calculateWindowLayoutInfo(@NonNull IBinder windowToken,
+ @NonNull List<CommonFoldingFeature> featureList) {
+ final Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken);
+ final SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo();
+ if (activity == null) {
+ return windowLayoutInfo;
+ }
+ windowLayoutInfo.displayFeatures = calculateDisplayFeatures(activity, featureList);
+ return windowLayoutInfo;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
index 8aca92e..ad913c9 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -35,8 +35,11 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.Activity;
import android.content.res.Configuration;
+import android.graphics.Color;
import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
import android.os.Binder;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
@@ -44,6 +47,8 @@
import android.view.Display;
import android.view.MotionEvent;
import android.view.SurfaceControl;
+import android.view.View;
+import android.view.Window;
import android.window.TaskFragmentOperation;
import android.window.TaskFragmentParentInfo;
import android.window.WindowContainerTransaction;
@@ -152,7 +157,10 @@
true /* isVerticalSplit */,
false /* isReversedLayout */,
Display.DEFAULT_DISPLAY,
- false /* isDraggableExpandType */);
+ false /* isDraggableExpandType */,
+ Color.valueOf(Color.BLACK), /* primaryVeilColor */
+ Color.valueOf(Color.GRAY) /* secondaryVeilColor */
+ );
mDividerPresenter = new DividerPresenter(
MOCK_TASK_ID, mDragEventCallback, mock(Executor.class));
@@ -604,6 +612,38 @@
0.0001 /* delta */);
}
+ @Test
+ public void testGetContainerBackgroundColor() {
+ final Color defaultColor = Color.valueOf(Color.RED);
+ final Color activityBackgroundColor = Color.valueOf(Color.BLUE);
+ final TaskFragmentContainer container = mock(TaskFragmentContainer.class);
+ final Activity activity = mock(Activity.class);
+ final Window window = mock(Window.class);
+ final View decorView = mock(View.class);
+ final ColorDrawable backgroundDrawable =
+ new ColorDrawable(activityBackgroundColor.toArgb());
+ when(activity.getWindow()).thenReturn(window);
+ when(window.getDecorView()).thenReturn(decorView);
+ when(decorView.getBackground()).thenReturn(backgroundDrawable);
+
+ // When the top non-finishing activity returns null, the default color should be returned.
+ when(container.getTopNonFinishingActivity()).thenReturn(null);
+ assertEquals(defaultColor,
+ DividerPresenter.getContainerBackgroundColor(container, defaultColor));
+
+ // When the top non-finishing activity is not resumed, the default color should be returned.
+ when(container.getTopNonFinishingActivity()).thenReturn(activity);
+ when(activity.isResumed()).thenReturn(false);
+ assertEquals(defaultColor,
+ DividerPresenter.getContainerBackgroundColor(container, defaultColor));
+
+ // When the top non-finishing activity is resumed, its background color should be returned.
+ when(container.getTopNonFinishingActivity()).thenReturn(activity);
+ when(activity.isResumed()).thenReturn(true);
+ assertEquals(activityBackgroundColor,
+ DividerPresenter.getContainerBackgroundColor(container, defaultColor));
+ }
+
private TaskFragmentContainer createMockTaskFragmentContainer(
@NonNull IBinder token, @NonNull Rect bounds) {
final TaskFragmentContainer container = mock(TaskFragmentContainer.class);
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
index 6110133..0764141 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
@@ -31,7 +31,6 @@
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner
import com.android.wm.shell.bubbles.DeviceConfig
-import com.android.wm.shell.bubbles.bar.BubbleExpandedViewPinController.Companion.DROP_TARGET_SCALE
import com.android.wm.shell.common.bubbles.BaseBubblePinController
import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION
import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION
@@ -248,15 +247,8 @@
private val dropTargetView: View?
get() = container.findViewById(R.id.bubble_bar_drop_target)
- private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect {
- val rect = Rect()
- positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, rect)
- // Scale the rect to expected size, but keep the center point the same
- val centerX = rect.centerX()
- val centerY = rect.centerY()
- rect.scale(DROP_TARGET_SCALE)
- rect.offset(centerX - rect.centerX(), centerY - rect.centerY())
- return rect
+ private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect = Rect().also {
+ positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, it)
}
private fun runOnMainSync(runnable: Runnable) {
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
index 9dcde3b..b928a0b 100644
--- a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
@@ -13,12 +13,14 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
- android:shape="rectangle">
- <corners android:radius="@dimen/bubble_bar_expanded_view_corner_radius" />
- <solid android:color="@color/bubble_drop_target_background_color" />
- <stroke
- android:width="1dp"
- android:color="?androidprv:attr/materialColorPrimaryContainer" />
-</shape>
+ android:inset="@dimen/bubble_bar_expanded_view_drop_target_padding">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/bubble_bar_expanded_view_drop_target_corner" />
+ <solid android:color="@color/bubble_drop_target_background_color" />
+ <stroke
+ android:width="1dp"
+ android:color="?androidprv:attr/materialColorPrimaryContainer" />
+ </shape>
+</inset>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index f532f96..8d24c16 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -274,6 +274,9 @@
<dimen name="bubble_bar_expanded_view_corner_radius">16dp</dimen>
<!-- Corner radius for expanded view while it is being dragged -->
<dimen name="bubble_bar_expanded_view_corner_radius_dragged">28dp</dimen>
+ <!-- Corner radius for expanded view drop target -->
+ <dimen name="bubble_bar_expanded_view_drop_target_corner">28dp</dimen>
+ <dimen name="bubble_bar_expanded_view_drop_target_padding">24dp</dimen>
<!-- Width of the box around bottom center of the screen where drag only leads to dismiss -->
<dimen name="bubble_bar_dismiss_zone_width">192dp</dimen>
<!-- Height of the box around bottom center of the screen where drag only leads to dismiss -->
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 87aac0b..9e6c5fb 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
@@ -170,6 +170,8 @@
* the pointer might need to be updated.
*/
void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer);
+ /** Called when the bubble overflow empty state changes, used to show/hide the overflow. */
+ void bubbleOverflowChanged(boolean hasBubbles);
}
private final Context mContext;
@@ -1169,6 +1171,15 @@
*/
public void startBubbleDrag(String bubbleKey) {
onBubbleDrag(bubbleKey, true /* isBeingDragged */);
+ if (mBubbleStateListener != null) {
+ boolean overflow = BubbleOverflow.KEY.equals(bubbleKey);
+ Rect rect = new Rect();
+ mBubblePositioner.getBubbleBarExpandedViewBounds(mBubblePositioner.isBubbleBarOnLeft(),
+ overflow, rect);
+ BubbleBarUpdate update = new BubbleBarUpdate();
+ update.expandedViewDropTargetSize = new Point(rect.width(), rect.height());
+ mBubbleStateListener.onBubbleStateChange(update);
+ }
}
/**
@@ -1401,7 +1412,7 @@
Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey);
if (b != null) {
// It's in the overflow, so remove it & reinflate
- mBubbleData.removeOverflowBubble(b);
+ mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL);
} else {
// App bubble does not exist, lets add and expand it
b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
@@ -1844,6 +1855,11 @@
}
}
+
+ @Override
+ public void bubbleOverflowChanged(boolean hasBubbles) {
+ // TODO (b/334175587): tell stack view to hide / show the overflow
+ }
};
/** When bubbles are in the bubble bar, this will be used to notify bubble bar views. */
@@ -1876,6 +1892,11 @@
}
@Override
+ public void bubbleOverflowChanged(boolean hasBubbles) {
+ // Nothing to do for our views, handled by launcher / in the bubble bar.
+ }
+
+ @Override
public void suppressionChanged(Bubble bubble, boolean isSuppressed) {
if (mLayerView != null) {
// TODO (b/273316505) handle suppression changes, although might not need to
@@ -1914,7 +1935,7 @@
ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:"
+ " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b"
+ " expanded=%b selectionChanged=%b selected=%s"
- + " suppressed=%s unsupressed=%s shouldShowEducation=%b",
+ + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b",
update.addedBubble != null ? update.addedBubble.getKey() : "null",
!update.removedBubbles.isEmpty(),
update.updatedBubble != null ? update.updatedBubble.getKey() : "null",
@@ -1923,13 +1944,17 @@
update.selectedBubble != null ? update.selectedBubble.getKey() : "null",
update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null",
update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null",
- update.shouldShowEducation);
+ update.shouldShowEducation, update.showOverflowChanged);
ensureBubbleViewsAndWindowCreated();
// Lazy load overflow bubbles from disk
loadOverflowBubblesFromDisk();
+ if (update.showOverflowChanged) {
+ mBubbleViewCallback.bubbleOverflowChanged(!update.overflowBubbles.isEmpty());
+ }
+
// If bubbles in the overflow have a dot, make sure the overflow shows a dot
updateOverflowButtonDot();
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 ae3d0c5..ceeed88 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
@@ -77,6 +77,7 @@
boolean suppressedSummaryChanged;
boolean expanded;
boolean shouldShowEducation;
+ boolean showOverflowChanged;
@Nullable BubbleViewProvider selectedBubble;
@Nullable Bubble addedBubble;
@Nullable Bubble updatedBubble;
@@ -109,7 +110,8 @@
|| suppressedBubble != null
|| unsuppressedBubble != null
|| suppressedSummaryChanged
- || suppressedSummaryGroup != null;
+ || suppressedSummaryGroup != null
+ || showOverflowChanged;
}
void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) {
@@ -410,6 +412,9 @@
if (bubbleToReturn != null) {
// Promoting from overflow
mOverflowBubbles.remove(bubbleToReturn);
+ if (mOverflowBubbles.isEmpty()) {
+ mStateChange.showOverflowChanged = true;
+ }
} else if (mPendingBubbles.containsKey(key)) {
// Update while it was pending
bubbleToReturn = mPendingBubbles.get(key);
@@ -497,19 +502,6 @@
}
/**
- * Explicitly removes a bubble from the overflow, if it exists.
- *
- * @param bubble the bubble to remove.
- */
- public void removeOverflowBubble(Bubble bubble) {
- if (bubble == null) return;
- if (mOverflowBubbles.remove(bubble)) {
- mStateChange.removedOverflowBubble = bubble;
- dispatchPendingChanges();
- }
- }
-
- /**
* Adds a group key indicating that the summary for this group should be suppressed.
*
* @param groupKey the group key of the group whose summary should be suppressed.
@@ -683,7 +675,6 @@
if (indexToRemove == -1) {
if (hasOverflowBubbleWithKey(key)
&& shouldRemoveHiddenBubble) {
-
Bubble b = getOverflowBubbleWithKey(key);
ProtoLog.d(WM_SHELL_BUBBLES, "doRemove - cancel overflow bubble=%s", key);
if (b != null) {
@@ -693,6 +684,7 @@
mOverflowBubbles.remove(b);
mStateChange.bubbleRemoved(b, reason);
mStateChange.removedOverflowBubble = b;
+ mStateChange.showOverflowChanged = mOverflowBubbles.isEmpty();
}
if (hasSuppressedBubbleWithKey(key) && shouldRemoveHiddenBubble) {
Bubble b = getSuppressedBubbleWithKey(key);
@@ -792,6 +784,9 @@
}
ProtoLog.d(WM_SHELL_BUBBLES, "overflowBubble=%s", bubble.getKey());
mLogger.logOverflowAdd(bubble, reason);
+ if (mOverflowBubbles.isEmpty()) {
+ mStateChange.showOverflowChanged = true;
+ }
mOverflowBubbles.remove(bubble);
mOverflowBubbles.add(0, bubble);
mStateChange.addedOverflowBubble = bubble;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 3c788b1..7bceb2c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -2341,8 +2341,8 @@
showScrim(true, null /* runnable */);
updateBubbleShadows(mIsExpanded);
- updateBadges(false /* setBadgeForCollapsedStack */);
mBubbleContainer.setActiveController(mExpandedAnimationController);
+ updateBadges(false /* setBadgeForCollapsedStack */);
updateOverflowVisibility();
updatePointerPosition(false /* forIme */);
mExpandedAnimationController.expandFromStack(() -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt
index 3b3974d..651bf02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt
@@ -22,7 +22,6 @@
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
-import androidx.annotation.VisibleForTesting
import androidx.core.view.updateLayoutParams
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner
@@ -79,7 +78,11 @@
override fun updateLocation(location: BubbleBarLocation) {
val view = dropTargetView ?: return
- getBounds(location.isOnLeft(view.isLayoutRtl), tempRect)
+ positioner.getBubbleBarExpandedViewBounds(
+ location.isOnLeft(view.isLayoutRtl),
+ false /* isOverflowExpanded */,
+ tempRect
+ )
view.updateLayoutParams<FrameLayout.LayoutParams> {
width = tempRect.width()
height = tempRect.height()
@@ -87,17 +90,4 @@
view.x = tempRect.left.toFloat()
view.y = tempRect.top.toFloat()
}
-
- private fun getBounds(onLeft: Boolean, out: Rect) {
- positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOverflowExpanded */, out)
- val centerX = out.centerX()
- val centerY = out.centerY()
- out.scale(DROP_TARGET_SCALE)
- // Move rect center back to the same position as before scale
- out.offset(centerX - out.centerX(), centerY - out.centerY())
- }
-
- companion object {
- @VisibleForTesting const val DROP_TARGET_SCALE = 0.9f
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index 98dccbb..da414cc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -389,9 +389,6 @@
public void dispatchDragEvent(DragEvent event) {}
@Override
- public void updatePointerIcon(float x, float y) {}
-
- @Override
public void dispatchWindowShown() {}
@Override
@@ -409,5 +406,10 @@
// ignore
}
}
+
+ @Override
+ public void dumpWindow(ParcelFileDescriptor pfd) {
+
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
index e5f6c37..6980c6f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.graphics.Point;
import android.os.Parcel;
import android.os.Parcelable;
@@ -49,6 +50,8 @@
public String unsupressedBubbleKey;
@Nullable
public BubbleBarLocation bubbleBarLocation;
+ @Nullable
+ public Point expandedViewDropTargetSize;
// This is only populated if bubbles have been removed.
public List<RemovedBubble> removedBubbles = new ArrayList<>();
@@ -81,12 +84,14 @@
suppressedBubbleKey = parcel.readString();
unsupressedBubbleKey = parcel.readString();
removedBubbles = parcel.readParcelableList(new ArrayList<>(),
- RemovedBubble.class.getClassLoader());
+ RemovedBubble.class.getClassLoader(), RemovedBubble.class);
parcel.readStringList(bubbleKeysInOrder);
currentBubbleList = parcel.readParcelableList(new ArrayList<>(),
- BubbleInfo.class.getClassLoader());
+ BubbleInfo.class.getClassLoader(), BubbleInfo.class);
bubbleBarLocation = parcel.readParcelable(BubbleBarLocation.class.getClassLoader(),
BubbleBarLocation.class);
+ expandedViewDropTargetSize = parcel.readParcelable(Point.class.getClassLoader(),
+ Point.class);
}
/**
@@ -105,6 +110,7 @@
|| bubbleBarLocation != null;
}
+ @NonNull
@Override
public String toString() {
return "BubbleBarUpdate{"
@@ -121,6 +127,7 @@
+ " bubbles=" + bubbleKeysInOrder
+ " currentBubbleList=" + currentBubbleList
+ " bubbleBarLocation=" + bubbleBarLocation
+ + " expandedViewDropTargetSize=" + expandedViewDropTargetSize
+ " }";
}
@@ -144,6 +151,7 @@
parcel.writeStringList(bubbleKeysInOrder);
parcel.writeParcelableList(currentBubbleList, flags);
parcel.writeParcelable(bubbleBarLocation, flags);
+ parcel.writeParcelable(expandedViewDropTargetSize, flags);
}
/**
@@ -157,10 +165,11 @@
@NonNull
public static final Creator<BubbleBarUpdate> CREATOR =
- new Creator<BubbleBarUpdate>() {
+ new Creator<>() {
public BubbleBarUpdate createFromParcel(Parcel source) {
return new BubbleBarUpdate(source);
}
+
public BubbleBarUpdate[] newArray(int size) {
return new BubbleBarUpdate[size];
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS
new file mode 100644
index 0000000..08c7031
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS
@@ -0,0 +1,6 @@
+# WM shell sub-module bubble owner
+madym@google.com
+atsjenk@google.com
+liranb@google.com
+sukeshram@google.com
+mpodolian@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
index 1e30d8f..ea86c79 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
@@ -21,6 +21,7 @@
import android.content.ComponentName
import android.content.Context
import android.os.RemoteException
+import android.os.SystemProperties
import android.util.DisplayMetrics
import android.util.Log
import android.util.Pair
@@ -137,5 +138,6 @@
@JvmStatic
val isPip2ExperimentEnabled: Boolean
- get() = Flags.enablePip2Implementation()
+ get() = Flags.enablePip2Implementation() || SystemProperties.getBoolean(
+ "wm_shell.pip2", false)
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index f195f95..3ab1fad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -81,6 +81,10 @@
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mCallback = callback;
mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
+ if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) {
+ // Don't show the SCM button for freeform tasks
+ mHasSizeCompat &= !taskInfo.isFreeform();
+ }
mCameraCompatControlState =
taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState;
mCompatUIHintsState = compatUIHintsState;
@@ -136,6 +140,10 @@
final boolean prevHasSizeCompat = mHasSizeCompat;
final int prevCameraCompatControlState = mCameraCompatControlState;
mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
+ if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) {
+ // Don't show the SCM button for freeform tasks
+ mHasSizeCompat &= !taskInfo.isFreeform();
+ }
mCameraCompatControlState =
taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
index 9bf9fa7..b41454d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
@@ -65,7 +65,7 @@
"persist.wm.debug.desktop_use_window_shadows_focused_window", false);
/**
- * Flag to indicate whether to apply shadows to windows in desktop mode.
+ * Flag to indicate whether to use rounded corners for windows in desktop mode.
*/
private static final boolean USE_ROUNDED_CORNERS = SystemProperties.getBoolean(
"persist.wm.debug.desktop_use_rounded_corners", true);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
new file mode 100644
index 0000000..6da3741
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -0,0 +1,173 @@
+/*
+ * 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:JvmName("DesktopModeUtils")
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.pm.ActivityInfo.isFixedOrientationLandscape
+import android.content.pm.ActivityInfo.isFixedOrientationPortrait
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
+import android.graphics.Rect
+import android.os.SystemProperties
+import android.util.Size
+import com.android.wm.shell.common.DisplayLayout
+
+
+val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float = SystemProperties
+ .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
+
+val DESKTOP_MODE_LANDSCAPE_APP_PADDING: Int = SystemProperties
+ .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25)
+
+
+/**
+ * Calculates the initial bounds required for an application to fill a scale of the display bounds
+ * without any letterboxing. This is done by taking into account the applications fullscreen size,
+ * aspect ratio, orientation and resizability to calculate an area this is compatible with the
+ * applications previous configuration.
+ */
+fun calculateInitialBounds(
+ displayLayout: DisplayLayout,
+ taskInfo: RunningTaskInfo,
+ scale: Float = DESKTOP_MODE_INITIAL_BOUNDS_SCALE
+): Rect {
+ val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height())
+ val appAspectRatio = calculateAspectRatio(taskInfo)
+ val idealSize = calculateIdealSize(screenBounds, scale)
+ // If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated.
+ // Instead default to the desired initial bounds.
+ val topActivityInfo = taskInfo.topActivityInfo
+ ?: return positionInScreen(idealSize, screenBounds)
+
+ val initialSize: Size = when (taskInfo.configuration.orientation) {
+ ORIENTATION_LANDSCAPE -> {
+ if (taskInfo.isResizeable) {
+ if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) {
+ // Respect apps fullscreen width
+ Size(taskInfo.appCompatTaskInfo.topActivityLetterboxWidth, idealSize.height)
+ } else {
+ idealSize
+ }
+ } else {
+ maximumSizeMaintainingAspectRatio(taskInfo, idealSize,
+ appAspectRatio)
+ }
+ }
+ ORIENTATION_PORTRAIT -> {
+ val customPortraitWidthForLandscapeApp = screenBounds.width() -
+ (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2)
+ if (taskInfo.isResizeable) {
+ if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+ // Respect apps fullscreen height and apply custom app width
+ Size(customPortraitWidthForLandscapeApp,
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight)
+ } else {
+ idealSize
+ }
+ } else {
+ if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+ // Apply custom app width and calculate maximum size
+ maximumSizeMaintainingAspectRatio(
+ taskInfo,
+ Size(customPortraitWidthForLandscapeApp, idealSize.height),
+ appAspectRatio)
+ } else {
+ maximumSizeMaintainingAspectRatio(taskInfo, idealSize,
+ appAspectRatio)
+ }
+ }
+ }
+ else -> {
+ idealSize
+ }
+ }
+
+ return positionInScreen(initialSize, screenBounds)
+}
+
+/**
+ * Calculates the largest size that can fit in a given area while maintaining a specific aspect
+ * ratio.
+ */
+private fun maximumSizeMaintainingAspectRatio(
+ taskInfo: RunningTaskInfo,
+ targetArea: Size,
+ aspectRatio: Float
+): Size {
+ val targetHeight = targetArea.height
+ val targetWidth = targetArea.width
+ val finalHeight: Int
+ val finalWidth: Int
+ if (isFixedOrientationPortrait(taskInfo.topActivityInfo!!.screenOrientation)) {
+ val tempWidth = (targetHeight / aspectRatio).toInt()
+ if (tempWidth <= targetWidth) {
+ finalHeight = targetHeight
+ finalWidth = tempWidth
+ } else {
+ finalWidth = targetWidth
+ finalHeight = (finalWidth * aspectRatio).toInt()
+ }
+ } else {
+ val tempWidth = (targetHeight * aspectRatio).toInt()
+ if (tempWidth <= targetWidth) {
+ finalHeight = targetHeight
+ finalWidth = tempWidth
+ } else {
+ finalWidth = targetWidth
+ finalHeight = (finalWidth / aspectRatio).toInt()
+ }
+ }
+ return Size(finalWidth, finalHeight)
+}
+
+/**
+ * Calculates the aspect ratio of an activity from its fullscreen bounds.
+ */
+private fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float {
+ if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) {
+ val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth
+ val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight
+ return maxOf(appLetterboxWidth, appLetterboxHeight) /
+ minOf(appLetterboxWidth, appLetterboxHeight).toFloat()
+ }
+ val appBounds = taskInfo.configuration.windowConfiguration.appBounds ?: return 1f
+ return maxOf(appBounds.height(), appBounds.width()) /
+ minOf(appBounds.height(), appBounds.width()).toFloat()
+}
+
+/**
+ * Calculates the desired initial bounds for applications in desktop windowing. This is done as a
+ * scale of the screen bounds.
+ */
+private fun calculateIdealSize(screenBounds: Rect, scale: Float): Size {
+ val width = (screenBounds.width() * scale).toInt()
+ val height = (screenBounds.height() * scale).toInt()
+ return Size(width, height)
+}
+
+/**
+ * Adjusts bounds to be positioned in the middle of the screen.
+ */
+private fun positionInScreen(desiredSize: Size, screenBounds: Rect): Rect {
+ // TODO(b/325240051): Position apps with bottom heavy offset
+ val heightOffset = (screenBounds.height() - desiredSize.height) / 2
+ val widthOffset = (screenBounds.width() - desiredSize.width) / 2
+ return Rect(widthOffset, heightOffset,
+ desiredSize.width + widthOffset, desiredSize.height + heightOffset)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index f7bfb86..b0d5923 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -47,6 +47,7 @@
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.annotation.BinderThread
+import com.android.internal.annotations.VisibleForTesting
import com.android.internal.policy.ScreenDecorationsUtils
import com.android.window.flags.Flags
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
@@ -85,7 +86,6 @@
import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
-import com.android.wm.shell.windowdecor.extension.isFreeform
import com.android.wm.shell.windowdecor.extension.isFullscreen
import java.io.PrintWriter
import java.util.Optional
@@ -203,6 +203,11 @@
dragAndDropController.addListener(this)
}
+ @VisibleForTesting
+ fun getVisualIndicator(): DesktopModeVisualIndicator? {
+ return visualIndicator
+ }
+
fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) {
toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
@@ -605,8 +610,9 @@
}
/**
- * Quick-resizes a desktop task, toggling between the stable bounds and the last saved bounds
- * if available or the default bounds otherwise.
+ * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the
+ * stable bounds) and a free floating state (either the last saved bounds if available or the
+ * default bounds otherwise).
*/
fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) {
val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
@@ -623,7 +629,11 @@
if (taskBoundsBeforeMaximize != null) {
destinationBounds.set(taskBoundsBeforeMaximize)
} else {
- destinationBounds.set(getDefaultDesktopTaskBounds(displayLayout))
+ if (Flags.enableWindowingDynamicInitialBounds()){
+ destinationBounds.set(calculateInitialBounds(displayLayout, taskInfo))
+ } else {
+ destinationBounds.set(getDefaultDesktopTaskBounds(displayLayout))
+ }
}
} else {
// Save current bounds so that task can be restored back to original bounds if necessary
@@ -1011,6 +1021,7 @@
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo
) {
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) {
@@ -1019,6 +1030,9 @@
} else {
WINDOWING_MODE_FREEFORM
}
+ if (Flags.enableWindowingDynamicInitialBounds()) {
+ wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo))
+ }
wct.setWindowingMode(taskInfo.token, targetWindowingMode)
wct.reorder(taskInfo.token, true /* onTop */)
if (isDesktopDensityOverrideSet()) {
@@ -1239,13 +1253,17 @@
* @param y height of drag, to be checked against status bar height.
*/
fun onDragPositioningEndThroughStatusBar(inputCoordinates: PointF, taskInfo: RunningTaskInfo) {
- val indicator = visualIndicator ?: return
+ val indicator = getVisualIndicator() ?: return
val indicatorType = indicator
.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
when (indicatorType) {
DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> {
val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
- finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
+ if (Flags.enableWindowingDynamicInitialBounds()) {
+ finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo))
+ } else {
+ finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
+ }
}
DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR,
DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> {
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 ad29d15..19af3d5 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
@@ -52,7 +52,7 @@
WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_SHELL),
WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
- Consts.TAG_WM_SHELL),
+ Consts.TAG_WM_DESKTOP_MODE),
WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_SHELL),
WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
@@ -120,6 +120,7 @@
private static final String TAG_WM_SHELL = "WindowManagerShell";
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 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/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index f3ef7c1..dfdb58a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -278,9 +278,11 @@
public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
if (visible && stage != STAGE_TYPE_UNDEFINED) {
DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId);
- if (decor != null && DesktopModeStatus.canEnterDesktopMode(mContext)) {
- mDesktopTasksController.moveToSplit(decor.mTaskInfo);
+ if (decor == null || !DesktopModeStatus.canEnterDesktopMode(mContext)
+ || decor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+ return;
}
+ mDesktopTasksController.moveToSplit(decor.mTaskInfo);
}
}
});
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index f7516da..4c347ad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -110,6 +110,8 @@
private ResizeVeil mResizeVeil;
private Bitmap mAppIconBitmap;
+ private Bitmap mResizeVeilBitmap;
+
private CharSequence mAppName;
private ExclusionRegionListener mExclusionRegionListener;
@@ -468,11 +470,15 @@
PackageManager pm = mContext.getApplicationContext().getPackageManager();
final IconProvider provider = new IconProvider(mContext);
final Drawable appIconDrawable = provider.getIcon(activityInfo);
- final Resources resources = mContext.getResources();
- final BaseIconFactory factory = new BaseIconFactory(mContext,
- resources.getDisplayMetrics().densityDpi,
- resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius));
- mAppIconBitmap = factory.createScaledBitmap(appIconDrawable, MODE_DEFAULT);
+ final BaseIconFactory headerIconFactory = createIconFactory(mContext,
+ R.dimen.desktop_mode_caption_icon_radius);
+ mAppIconBitmap = headerIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT);
+
+ final BaseIconFactory resizeVeilIconFactory = createIconFactory(mContext,
+ R.dimen.desktop_mode_resize_veil_icon_size);
+ mResizeVeilBitmap = resizeVeilIconFactory
+ .createScaledBitmap(appIconDrawable, MODE_DEFAULT);
+
final ApplicationInfo applicationInfo = activityInfo.applicationInfo;
mAppName = pm.getApplicationLabel(applicationInfo);
} finally {
@@ -480,6 +486,13 @@
}
}
+ private BaseIconFactory createIconFactory(Context context, int dimensions) {
+ final Resources resources = context.getResources();
+ final int densityDpi = resources.getDisplayMetrics().densityDpi;
+ final int iconSize = resources.getDimensionPixelSize(dimensions);
+ return new BaseIconFactory(context, densityDpi, iconSize);
+ }
+
private void closeDragResizeListener() {
if (mDragResizeListener == null) {
return;
@@ -495,7 +508,7 @@
private void createResizeVeilIfNeeded() {
if (mResizeVeil != null) return;
loadAppInfoIfNeeded();
- mResizeVeil = new ResizeVeil(mContext, mDisplayController, mAppIconBitmap, mTaskInfo,
+ mResizeVeil = new ResizeVeil(mContext, mDisplayController, mResizeVeilBitmap, mTaskInfo,
mTaskSurface, mSurfaceControlTransactionSupplier);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index 9624d46..5379ca6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -24,7 +24,7 @@
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_CONSUMER;
-import static com.android.input.flags.Flags.enablePointerChoreographer;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT;
@@ -54,6 +54,7 @@
import android.view.WindowManagerGlobal;
import android.window.InputTransferToken;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
@@ -399,12 +400,17 @@
float rawX = e.getRawX(0);
float rawY = e.getRawY(0);
int ctrlType = mDragResizeWindowGeometry.calculateCtrlType(isTouch, x, y);
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "%s: Handling action down, update ctrlType to %d", TAG, ctrlType);
mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType,
rawX, rawY);
// Increase the input sink region to cover the whole screen; this is to
// prevent input and focus from going to other tasks during a drag resize.
updateInputSinkRegionForDrag(mDragStartTaskBounds);
result = true;
+ } else {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "%s: Handling action down, but ignore event", TAG);
}
break;
}
@@ -499,12 +505,10 @@
// where views in the task can receive input events because we can't set touch regions
// of input sinks to have rounded corners.
if (mLastCursorType != cursorType || cursorType != PointerIcon.TYPE_DEFAULT) {
- if (enablePointerChoreographer()) {
- mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, cursorType),
- displayId, deviceId, pointerId, mInputChannel.getToken());
- } else {
- mInputManager.setPointerIconType(cursorType);
- }
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: update pointer icon from %d to %d",
+ TAG, mLastCursorType, cursorType);
+ mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, cursorType),
+ displayId, deviceId, pointerId, mInputChannel.getToken());
mLastCursorType = cursorType;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index eafb569..4f513f0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -33,6 +33,9 @@
import android.util.Size;
import android.view.MotionEvent;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
import com.android.wm.shell.R;
import java.util.Objects;
@@ -41,6 +44,11 @@
* Geometry for a drag resize region for a particular window.
*/
final class DragResizeWindowGeometry {
+ // TODO(b/337264971) clean up when no longer needed
+ @VisibleForTesting static final boolean DEBUG = true;
+ // The additional width to apply to edge resize bounds just for logging when a touch is
+ // close.
+ @VisibleForTesting static final int EDGE_DEBUG_BUFFER = 15;
private final int mTaskCornerRadius;
private final Size mTaskSize;
// The size of the handle applied to the edges of the window, for the user to drag resize.
@@ -51,10 +59,9 @@
// The task corners to permit drag resizing with a fine input, such as stylus or cursor.
private final @NonNull TaskCorners mFineTaskCorners;
// The bounds for each edge drag region, which can resize the task in one direction.
- private final @NonNull Rect mTopEdgeBounds;
- private final @NonNull Rect mLeftEdgeBounds;
- private final @NonNull Rect mRightEdgeBounds;
- private final @NonNull Rect mBottomEdgeBounds;
+ private final @NonNull TaskEdges mTaskEdges;
+ // Extra-large edge bounds for logging to help debug when an edge resize is ignored.
+ private final @Nullable TaskEdges mDebugTaskEdges;
DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize,
int resizeHandleThickness, int fineCornerSize, int largeCornerSize) {
@@ -66,26 +73,12 @@
mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize);
// Save touch areas for each edge.
- mTopEdgeBounds = new Rect(
- -mResizeHandleThickness,
- -mResizeHandleThickness,
- mTaskSize.getWidth() + mResizeHandleThickness,
- 0);
- mLeftEdgeBounds = new Rect(
- -mResizeHandleThickness,
- 0,
- 0,
- mTaskSize.getHeight());
- mRightEdgeBounds = new Rect(
- mTaskSize.getWidth(),
- 0,
- mTaskSize.getWidth() + mResizeHandleThickness,
- mTaskSize.getHeight());
- mBottomEdgeBounds = new Rect(
- -mResizeHandleThickness,
- mTaskSize.getHeight(),
- mTaskSize.getWidth() + mResizeHandleThickness,
- mTaskSize.getHeight() + mResizeHandleThickness);
+ mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness);
+ if (DEBUG) {
+ mDebugTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness + EDGE_DEBUG_BUFFER);
+ } else {
+ mDebugTaskEdges = null;
+ }
}
/**
@@ -127,10 +120,13 @@
*/
void union(@NonNull Region region) {
// Apply the edge resize regions.
- region.union(mTopEdgeBounds);
- region.union(mLeftEdgeBounds);
- region.union(mRightEdgeBounds);
- region.union(mBottomEdgeBounds);
+ if (inDebugMode()) {
+ // Use the larger edge sizes if we are debugging, to be able to log if we ignored a
+ // touch due to the size of the edge region.
+ mDebugTaskEdges.union(region);
+ } else {
+ mTaskEdges.union(region);
+ }
if (enableWindowingEdgeDragResize()) {
// Apply the corners as well for the larger corners, to ensure we capture all possible
@@ -216,6 +212,10 @@
@DragPositioningCallback.CtrlType
private int calculateEdgeResizeCtrlType(float x, float y) {
+ if (inDebugMode() && (mDebugTaskEdges.contains((int) x, (int) y)
+ && !mTaskEdges.contains((int) x, (int) y))) {
+ return CTRL_TYPE_UNDEFINED;
+ }
int ctrlType = CTRL_TYPE_UNDEFINED;
// mTaskCornerRadius is only used in comparing with corner regions. Comparisons with
// sides will use the bounds specified in setGeometry and not go into task bounds.
@@ -306,10 +306,9 @@
&& this.mResizeHandleThickness == other.mResizeHandleThickness
&& this.mFineTaskCorners.equals(other.mFineTaskCorners)
&& this.mLargeTaskCorners.equals(other.mLargeTaskCorners)
- && this.mTopEdgeBounds.equals(other.mTopEdgeBounds)
- && this.mLeftEdgeBounds.equals(other.mLeftEdgeBounds)
- && this.mRightEdgeBounds.equals(other.mRightEdgeBounds)
- && this.mBottomEdgeBounds.equals(other.mBottomEdgeBounds);
+ && (inDebugMode()
+ ? this.mDebugTaskEdges.equals(other.mDebugTaskEdges)
+ : this.mTaskEdges.equals(other.mTaskEdges));
}
@Override
@@ -320,10 +319,11 @@
mResizeHandleThickness,
mFineTaskCorners,
mLargeTaskCorners,
- mTopEdgeBounds,
- mLeftEdgeBounds,
- mRightEdgeBounds,
- mBottomEdgeBounds);
+ (inDebugMode() ? mDebugTaskEdges : mTaskEdges));
+ }
+
+ private boolean inDebugMode() {
+ return DEBUG && mDebugTaskEdges != null;
}
/**
@@ -431,4 +431,92 @@
mRightBottomCornerBounds);
}
}
+
+ /**
+ * Representation of the drag resize regions at the edges of the window.
+ */
+ private static class TaskEdges {
+ private final @NonNull Rect mTopEdgeBounds;
+ private final @NonNull Rect mLeftEdgeBounds;
+ private final @NonNull Rect mRightEdgeBounds;
+ private final @NonNull Rect mBottomEdgeBounds;
+ private final @NonNull Region mRegion;
+
+ private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness) {
+ // Save touch areas for each edge.
+ mTopEdgeBounds = new Rect(
+ -resizeHandleThickness,
+ -resizeHandleThickness,
+ taskSize.getWidth() + resizeHandleThickness,
+ 0);
+ mLeftEdgeBounds = new Rect(
+ -resizeHandleThickness,
+ 0,
+ 0,
+ taskSize.getHeight());
+ mRightEdgeBounds = new Rect(
+ taskSize.getWidth(),
+ 0,
+ taskSize.getWidth() + resizeHandleThickness,
+ taskSize.getHeight());
+ mBottomEdgeBounds = new Rect(
+ -resizeHandleThickness,
+ taskSize.getHeight(),
+ taskSize.getWidth() + resizeHandleThickness,
+ taskSize.getHeight() + resizeHandleThickness);
+
+ mRegion = new Region();
+ mRegion.union(mTopEdgeBounds);
+ mRegion.union(mLeftEdgeBounds);
+ mRegion.union(mRightEdgeBounds);
+ mRegion.union(mBottomEdgeBounds);
+ }
+
+ /**
+ * Returns {@code true} if the edges contain the given point.
+ */
+ private boolean contains(int x, int y) {
+ return mRegion.contains(x, y);
+ }
+
+ /**
+ * Updates the region to include all four corners.
+ */
+ private void union(Region region) {
+ region.union(mTopEdgeBounds);
+ region.union(mLeftEdgeBounds);
+ region.union(mRightEdgeBounds);
+ region.union(mBottomEdgeBounds);
+ }
+
+ @Override
+ public String toString() {
+ return "TaskEdges for the"
+ + " top " + mTopEdgeBounds
+ + " left " + mLeftEdgeBounds
+ + " right " + mRightEdgeBounds
+ + " bottom " + mBottomEdgeBounds;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (this == obj) return true;
+ if (!(obj instanceof TaskEdges other)) return false;
+
+ return this.mTopEdgeBounds.equals(other.mTopEdgeBounds)
+ && this.mLeftEdgeBounds.equals(other.mLeftEdgeBounds)
+ && this.mRightEdgeBounds.equals(other.mRightEdgeBounds)
+ && this.mBottomEdgeBounds.equals(other.mBottomEdgeBounds);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mTopEdgeBounds,
+ mLeftEdgeBounds,
+ mRightEdgeBounds,
+ mBottomEdgeBounds);
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
index a2293d5..ec20471 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.windowdecor.extension
import android.app.TaskInfo
-import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS
import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND
@@ -36,6 +35,3 @@
val TaskInfo.isFullscreen: Boolean
get() = windowingMode == WINDOWING_MODE_FULLSCREEN
-
-val TaskInfo.isFreeform: Boolean
- get() = windowingMode == WINDOWING_MODE_FREEFORM
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 6be411d..0f43377 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -1193,23 +1193,6 @@
}
@Test
- public void test_removeOverflowBubble() {
- sendUpdatedEntryAtTime(mEntryA1, 2000);
- mBubbleData.setListener(mListener);
-
- mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
- verifyUpdateReceived();
- assertOverflowChangedTo(ImmutableList.of(mBubbleA1));
-
- mBubbleData.removeOverflowBubble(mBubbleA1);
- verifyUpdateReceived();
-
- BubbleData.Update update = mUpdateCaptor.getValue();
- assertThat(update.removedOverflowBubble).isEqualTo(mBubbleA1);
- assertOverflowChangedTo(ImmutableList.of());
- }
-
- @Test
public void test_getInitialStateForBubbleBar_includesInitialBubblesAndPosition() {
sendUpdatedEntryAtTime(mEntryA1, 1000);
sendUpdatedEntryAtTime(mEntryA2, 2000);
@@ -1235,6 +1218,51 @@
assertExpandedChangedTo(true);
}
+ @Test
+ public void testShowOverflowChanged_hasOverflowBubbles() {
+ assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ mBubbleData.setListener(mListener);
+
+ mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
+ verifyUpdateReceived();
+ assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue();
+ assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+ }
+
+ @Test
+ public void testShowOverflowChanged_false_hasOverflowBubbles() {
+ assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ sendUpdatedEntryAtTime(mEntryA2, 1000);
+ mBubbleData.setListener(mListener);
+
+ // First overflowed causes change event
+ mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
+ verifyUpdateReceived();
+ assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue();
+ assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+
+ // Second overflow does not
+ mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE);
+ verifyUpdateReceived();
+ assertThat(mUpdateCaptor.getValue().showOverflowChanged).isFalse();
+ }
+
+ @Test
+ public void testShowOverflowChanged_noOverflowBubbles() {
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
+ assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+ mBubbleData.setListener(mListener);
+
+ mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
+
+ verifyUpdateReceived();
+ assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue();
+ assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+ }
+
private void verifyUpdateReceived() {
verify(mListener).applyUpdate(mUpdateCaptor.capture());
reset(mListener);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index df8a222..3f76c4f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -24,6 +24,12 @@
import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
@@ -101,7 +107,9 @@
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.capture
import org.mockito.quality.Strictness
@@ -141,6 +149,7 @@
@Mock lateinit var dragAndDropController: DragAndDropController
@Mock lateinit var multiInstanceHelper: MultiInstanceHelper
@Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver
+ @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
private lateinit var mockitoSession: StaticMockitoSession
private lateinit var controller: DesktopTasksController
@@ -154,6 +163,15 @@
// Mock running tasks are registered here so we can get the list from mock shell task organizer
private val runningTasks = mutableListOf<RunningTaskInfo>()
+ private val DISPLAY_DIMENSION_SHORT = 1600
+ private val DISPLAY_DIMENSION_LONG = 2560
+ private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 200, 2240, 1400)
+ private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240)
+ private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880)
+ private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400)
+ private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861)
+ private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400)
+
@Before
fun setUp() {
mockitoSession = mockitoSession().strictness(Strictness.LENIENT)
@@ -161,7 +179,7 @@
whenever(DesktopModeStatus.isEnabled()).thenReturn(true)
doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- shellInit = Mockito.spy(ShellInit(testExecutor))
+ shellInit = spy(ShellInit(testExecutor))
desktopModeTaskRepository = DesktopModeTaskRepository()
desktopTasksLimiter =
DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer)
@@ -464,6 +482,135 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask()
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+ shouldLetterbox = true)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(isResizable = false,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(isResizable = false,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(isResizable = false,
+ deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(isResizable = false,
+ deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
val task = setUpFullscreenTask()
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -1225,6 +1372,185 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask()
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+ shouldLetterbox = true)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(isResizable = false,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(isResizable = false,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(isResizable = false,
+ deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(isResizable = false,
+ deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() {
val task = setUpFreeformTask()
val mockSurface = mock(SurfaceControl::class.java)
@@ -1276,8 +1602,7 @@
controller.toggleDesktopTaskSize(task)
// Assert bounds set to stable bounds
val wct = getLatestToggleResizeDesktopTaskWct()
- assertThat(wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds)
- .isEqualTo(STABLE_BOUNDS)
+ assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
}
@Test
@@ -1304,8 +1629,7 @@
// Assert bounds set to last bounds before maximize
val wct = getLatestToggleResizeDesktopTaskWct()
- assertThat(wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds)
- .isEqualTo(boundsBeforeMaximize)
+ assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
}
@Test
@@ -1346,14 +1670,65 @@
return task
}
- private fun setUpFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+ private fun setUpFullscreenTask(
+ displayId: Int = DEFAULT_DISPLAY,
+ isResizable: Boolean = true,
+ windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
+ deviceOrientation: Int = ORIENTATION_LANDSCAPE,
+ screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED,
+ shouldLetterbox: Boolean = false
+ ): RunningTaskInfo {
val task = createFullscreenTask(displayId)
+ val activityInfo = ActivityInfo()
+ activityInfo.screenOrientation = screenOrientation
+ with(task) {
+ topActivityInfo = activityInfo
+ isResizeable = isResizable
+ configuration.orientation = deviceOrientation
+ configuration.windowConfiguration.windowingMode = windowingMode
+
+ if (shouldLetterbox) {
+ if (deviceOrientation == ORIENTATION_LANDSCAPE &&
+ screenOrientation == SCREEN_ORIENTATION_PORTRAIT) {
+ // Letterbox to portrait size
+ appCompatTaskInfo.topActivityBoundsLetterboxed = true
+ appCompatTaskInfo.topActivityLetterboxWidth = 1200
+ appCompatTaskInfo.topActivityLetterboxHeight = 1600
+ } else if (deviceOrientation == ORIENTATION_PORTRAIT &&
+ screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) {
+ // Letterbox to landscape size
+ appCompatTaskInfo.topActivityBoundsLetterboxed = true
+ appCompatTaskInfo.topActivityLetterboxWidth = 1600
+ appCompatTaskInfo.topActivityLetterboxHeight = 1200
+ }
+ } else {
+ appCompatTaskInfo.topActivityBoundsLetterboxed = false
+ }
+
+ if (deviceOrientation == ORIENTATION_LANDSCAPE) {
+ configuration.windowConfiguration.appBounds = Rect(0, 0,
+ DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
+ } else {
+ configuration.windowConfiguration.appBounds = Rect(0, 0,
+ DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG)
+ }
+ }
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
runningTasks.add(task)
return task
}
+ private fun setUpLandscapeDisplay() {
+ whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG)
+ whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT)
+ }
+
+ private fun setUpPortraitDisplay() {
+ whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT)
+ whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG)
+ }
+
private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
val task = createSplitScreenTask(displayId)
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
@@ -1418,6 +1793,17 @@
return arg.value
}
+ private fun getLatestDragToDesktopWct(): WindowContainerTransaction {
+ val arg: ArgumentCaptor<WindowContainerTransaction> =
+ ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
+ } else {
+ verify(shellTaskOrganizer).applyTransaction(capture(arg))
+ }
+ return arg.value
+ }
+
private fun getLatestExitDesktopWct(): WindowContainerTransaction {
val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
if (ENABLE_SHELL_TRANSITIONS) {
@@ -1429,6 +1815,10 @@
return arg.value
}
+ private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
+ wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
+
+
private fun verifyWCTNotExecuted() {
if (ENABLE_SHELL_TRANSITIONS) {
verify(transitions, never()).startTransition(anyInt(), any(), isNull())
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
index 82e5a1c..5464508 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
@@ -56,6 +56,8 @@
private static final Size TASK_SIZE = new Size(500, 1000);
private static final int TASK_CORNER_RADIUS = 10;
private static final int EDGE_RESIZE_THICKNESS = 15;
+ private static final int EDGE_RESIZE_DEBUG_THICKNESS = EDGE_RESIZE_THICKNESS
+ + (DragResizeWindowGeometry.DEBUG ? DragResizeWindowGeometry.EDGE_DEBUG_BUFFER : 0);
private static final int FINE_CORNER_SIZE = EDGE_RESIZE_THICKNESS * 2 + 10;
private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10;
private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry(
@@ -90,13 +92,14 @@
EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE),
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE))
- .addEqualityGroup(
+ .addEqualityGroup(new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+ EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE + 5),
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
- EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE,
- LARGE_CORNER_SIZE + 5),
+ EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE + 5))
+ .addEqualityGroup(new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+ EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE + 4, LARGE_CORNER_SIZE),
new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
- EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE,
- LARGE_CORNER_SIZE + 5))
+ EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE + 4, LARGE_CORNER_SIZE))
.testEquals();
}
@@ -122,21 +125,21 @@
private static void verifyHorizontalEdge(@NonNull Region region, @NonNull Point point) {
assertThat(region.contains(point.x, point.y)).isTrue();
// Horizontally along the edge is still contained.
- assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isTrue();
- assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isTrue();
+ assertThat(region.contains(point.x + EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isTrue();
+ assertThat(region.contains(point.x - EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isTrue();
// Vertically along the edge is not contained.
- assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isFalse();
- assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isFalse();
+ assertThat(region.contains(point.x, point.y - EDGE_RESIZE_DEBUG_THICKNESS)).isFalse();
+ assertThat(region.contains(point.x, point.y + EDGE_RESIZE_DEBUG_THICKNESS)).isFalse();
}
private static void verifyVerticalEdge(@NonNull Region region, @NonNull Point point) {
assertThat(region.contains(point.x, point.y)).isTrue();
// Horizontally along the edge is not contained.
- assertThat(region.contains(point.x + EDGE_RESIZE_THICKNESS, point.y)).isFalse();
- assertThat(region.contains(point.x - EDGE_RESIZE_THICKNESS, point.y)).isFalse();
+ assertThat(region.contains(point.x + EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isFalse();
+ assertThat(region.contains(point.x - EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isFalse();
// Vertically along the edge is contained.
- assertThat(region.contains(point.x, point.y - EDGE_RESIZE_THICKNESS)).isTrue();
- assertThat(region.contains(point.x, point.y + EDGE_RESIZE_THICKNESS)).isTrue();
+ assertThat(region.contains(point.x, point.y - EDGE_RESIZE_DEBUG_THICKNESS)).isTrue();
+ assertThat(region.contains(point.x, point.y + EDGE_RESIZE_DEBUG_THICKNESS)).isTrue();
}
/**
@@ -148,7 +151,10 @@
public void testRegionUnion_edgeDragResizeEnabled_containsLargeCorners() {
Region region = new Region();
GEOMETRY.union(region);
- final int cornerRadius = LARGE_CORNER_SIZE / 2;
+ // Make sure we're choosing a point outside of any debug region buffer.
+ final int cornerRadius = DragResizeWindowGeometry.DEBUG
+ ? Math.max(LARGE_CORNER_SIZE / 2, EDGE_RESIZE_DEBUG_THICKNESS)
+ : LARGE_CORNER_SIZE / 2;
new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region);
}
@@ -162,7 +168,9 @@
public void testRegionUnion_edgeDragResizeDisabled_containsFineCorners() {
Region region = new Region();
GEOMETRY.union(region);
- final int cornerRadius = FINE_CORNER_SIZE / 2;
+ final int cornerRadius = DragResizeWindowGeometry.DEBUG
+ ? Math.max(LARGE_CORNER_SIZE / 2, EDGE_RESIZE_DEBUG_THICKNESS)
+ : LARGE_CORNER_SIZE / 2;
new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region);
}
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index b2838c8..7ddf11e 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -50,6 +50,7 @@
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.media.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -984,37 +985,7 @@
@SystemApi
@RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL)
public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) {
- mImpl.transfer(
- controller.getRoutingSessionInfo(),
- route,
- Process.myUserHandle(),
- mContext.getPackageName());
- }
-
- /**
- * Transfers the media of a routing controller to the given route.
- *
- * <p>This will be no-op for non-system media routers.
- *
- * @param controller a routing controller controlling media routing.
- * @param route the route you want to transfer the media to.
- * @param transferInitiatorUserHandle the user handle of the app that initiated the transfer
- * request.
- * @param transferInitiatorPackageName the package name of the app that initiated the transfer.
- * This value is used with the user handle to populate {@link
- * RoutingController#wasTransferInitiatedBySelf()}.
- * @hide
- */
- public void transfer(
- @NonNull RoutingController controller,
- @NonNull MediaRoute2Info route,
- @NonNull UserHandle transferInitiatorUserHandle,
- @NonNull String transferInitiatorPackageName) {
- mImpl.transfer(
- controller.getRoutingSessionInfo(),
- route,
- transferInitiatorUserHandle,
- transferInitiatorPackageName);
+ mImpl.transfer(controller.getRoutingSessionInfo(), route);
}
void requestCreateController(
@@ -1913,13 +1884,7 @@
*/
@FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
public boolean wasTransferInitiatedBySelf() {
- RoutingSessionInfo sessionInfo = getRoutingSessionInfo();
-
- UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
- String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
-
- return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle)
- && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName);
+ return mImpl.wasTransferredBySelf(getRoutingSessionInfo());
}
/**
@@ -2082,12 +2047,28 @@
Objects.requireNonNull(route, "route must not be null");
synchronized (mControllerLock) {
if (isReleased()) {
- Log.w(TAG, "transferToRoute: Called on released controller. Ignoring.");
+ Log.w(
+ TAG,
+ "tryTransferWithinProvider: Called on released controller. Ignoring.");
return true;
}
- if (!mSessionInfo.getTransferableRoutes().contains(route.getId())) {
- Log.w(TAG, "Ignoring transferring to a non-transferable route=" + route);
+ // If this call is trying to transfer to a selected system route, we let them
+ // through as a provider driven transfer in order to update the transfer reason and
+ // initiator data.
+ boolean isSystemRouteReselection =
+ Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
+ && mSessionInfo.isSystemSession()
+ && route.isSystemRoute()
+ && mSessionInfo.getSelectedRoutes().contains(route.getId());
+ if (!isSystemRouteReselection
+ && !mSessionInfo.getTransferableRoutes().contains(route.getId())) {
+ Log.i(
+ TAG,
+ "Transferring to a non-transferable route="
+ + route
+ + " session= "
+ + mSessionInfo.getId());
return false;
}
}
@@ -2498,11 +2479,7 @@
void stop();
- void transfer(
- @NonNull RoutingSessionInfo sessionInfo,
- @NonNull MediaRoute2Info route,
- @NonNull UserHandle transferInitiatorUserHandle,
- @NonNull String transferInitiatorPackageName);
+ void transfer(@NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route);
List<RoutingController> getControllers();
@@ -2523,6 +2500,11 @@
boolean shouldNotifyStop,
RoutingController controller);
+ /**
+ * Returns the value of {@link RoutingController#wasTransferInitiatedBySelf()} for the app
+ * associated with this router.
+ */
+ boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo);
}
/**
@@ -2723,7 +2705,7 @@
List<RoutingSessionInfo> sessionInfos = getRoutingSessions();
RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
- transfer(targetSession, route, mClientUser, mContext.getPackageName());
+ transfer(targetSession, route);
}
@Override
@@ -2746,24 +2728,15 @@
*
* @param sessionInfo The {@link RoutingSessionInfo routing session} to transfer.
* @param route The {@link MediaRoute2Info route} to transfer to.
- * @param transferInitiatorUserHandle The user handle of the app that initiated the
- * transfer.
- * @param transferInitiatorPackageName The package name if of the app that initiated the
- * transfer.
* @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info, UserHandle, String)
* @see #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)
*/
@Override
@SuppressWarnings("AndroidFrameworkRequiresPermission")
public void transfer(
- @NonNull RoutingSessionInfo sessionInfo,
- @NonNull MediaRoute2Info route,
- @NonNull UserHandle transferInitiatorUserHandle,
- @NonNull String transferInitiatorPackageName) {
+ @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
Objects.requireNonNull(route, "route must not be null");
- Objects.requireNonNull(transferInitiatorUserHandle);
- Objects.requireNonNull(transferInitiatorPackageName);
Log.v(
TAG,
@@ -2780,15 +2753,19 @@
return;
}
- if (sessionInfo.getTransferableRoutes().contains(route.getId())) {
- transferToRoute(
- sessionInfo,
- route,
- transferInitiatorUserHandle,
- transferInitiatorPackageName);
+ // If this call is trying to transfer to a selected system route, we let them
+ // through as a provider driven transfer in order to update the transfer reason and
+ // initiator data.
+ boolean isSystemRouteReselection =
+ Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
+ && sessionInfo.isSystemSession()
+ && route.isSystemRoute()
+ && sessionInfo.getSelectedRoutes().contains(route.getId());
+ if (sessionInfo.getTransferableRoutes().contains(route.getId())
+ || isSystemRouteReselection) {
+ transferToRoute(sessionInfo, route, mClientUser, mClientPackageName);
} else {
- requestCreateSession(sessionInfo, route, transferInitiatorUserHandle,
- transferInitiatorPackageName);
+ requestCreateSession(sessionInfo, route, mClientUser, mClientPackageName);
}
}
@@ -3043,6 +3020,14 @@
releaseSession(controller.getRoutingSessionInfo());
}
+ @Override
+ public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) {
+ UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
+ String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
+ return Objects.equals(mClientUser, transferInitiatorUserHandle)
+ && Objects.equals(mClientPackageName, transferInitiatorPackageName);
+ }
+
/**
* Retrieves the system session info for the given package.
*
@@ -3619,10 +3604,7 @@
*/
@Override
public void transfer(
- @NonNull RoutingSessionInfo sessionInfo,
- @NonNull MediaRoute2Info route,
- @NonNull UserHandle transferInitiatorUserHandle,
- @NonNull String transferInitiatorPackageName) {
+ @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
// Do nothing.
}
@@ -3741,6 +3723,14 @@
}
}
+ @Override
+ public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) {
+ UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
+ String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
+ return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle)
+ && Objects.equals(mContext.getPackageName(), transferInitiatorPackageName);
+ }
+
@GuardedBy("mLock")
private void registerRouterStubIfNeededLocked() throws RemoteException {
if (mStub == null) {
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
index d969d1c..2e9b7b4 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt
@@ -446,6 +446,9 @@
// Returns InstallUserActionRequired stage if install details could be successfully
// computed, else it returns InstallAborted.
val confirmationSnippet: InstallStage = generateConfirmationSnippet()
+ if (confirmationSnippet.stageCode == InstallStage.STAGE_ABORTED) {
+ return confirmationSnippet
+ }
val existingUpdateOwner: CharSequence? = getExistingUpdateOwner(newPackageInfo!!)
return if (sessionId == SessionInfo.INVALID_ID &&
diff --git a/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml
index 7c76ea1..221e8f5 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-night-v35/colors.xml
@@ -38,7 +38,7 @@
<color name="settingslib_track_off_color">@color/settingslib_materialColorSurfaceContainerHighest</color>
<!-- Dialog background color. -->
- <color name="settingslib_dialog_background">@color/settingslib_materialColorSurfaceInverse</color>
+ <color name="settingslib_dialog_background">@color/settingslib_materialColorSurfaceContainer</color>
<color name="settingslib_colorSurfaceHeader">@color/settingslib_materialColorSurfaceVariant</color>
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml
index 2a6499a..dc2d3dc 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/colors.xml
@@ -38,7 +38,7 @@
<color name="settingslib_track_off_color">@color/settingslib_materialColorSurfaceContainerHighest</color>
<!-- Dialog background color. -->
- <color name="settingslib_dialog_background">@color/settingslib_materialColorSurfaceInverse</color>
+ <color name="settingslib_dialog_background">@color/settingslib_materialColorSurfaceContainer</color>
<!-- Material next track outline color-->
<color name="settingslib_track_online_color">@color/settingslib_switch_track_outline_color</color>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 3dffb27..8917412 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -10,8 +10,10 @@
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -37,12 +39,9 @@
import com.android.settingslib.widget.AdaptiveIcon;
import com.android.settingslib.widget.AdaptiveOutlineDrawable;
-import com.google.common.collect.ImmutableSet;
-
import java.io.IOException;
import java.util.List;
import java.util.Locale;
-import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -56,8 +55,6 @@
public static final String BT_ADVANCED_HEADER_ENABLED = "bt_advanced_header_enabled";
private static final int METADATA_FAST_PAIR_CUSTOMIZED_FIELDS = 25;
private static final String KEY_HEARABLE_CONTROL_SLICE = "HEARABLE_CONTROL_SLICE_WITH_WIDTH";
- private static final Set<String> EXCLUSIVE_MANAGERS =
- ImmutableSet.of("com.google.android.gms.dck");
private static ErrorListener sErrorListener;
@@ -740,14 +737,13 @@
/**
* Returns the BluetoothDevice's exclusive manager ({@link
- * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists and is in the given
- * set, otherwise null.
+ * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata) if it exists, otherwise null.
*/
@Nullable
- private static String getAllowedExclusiveManager(BluetoothDevice bluetoothDevice) {
- byte[] exclusiveManagerNameBytes =
+ private static String getExclusiveManager(BluetoothDevice bluetoothDevice) {
+ byte[] exclusiveManagerBytes =
bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER);
- if (exclusiveManagerNameBytes == null) {
+ if (exclusiveManagerBytes == null) {
Log.d(
TAG,
"Bluetooth device "
@@ -755,47 +751,46 @@
+ " doesn't have exclusive manager");
return null;
}
- String exclusiveManagerName = new String(exclusiveManagerNameBytes);
- return getExclusiveManagers().contains(exclusiveManagerName) ? exclusiveManagerName : null;
+ return new String(exclusiveManagerBytes);
}
- /** Checks if given package is installed */
- private static boolean isPackageInstalled(Context context, String packageName) {
+ /** Checks if given package is installed and enabled */
+ private static boolean isPackageInstalledAndEnabled(Context context, String packageName) {
PackageManager packageManager = context.getPackageManager();
try {
- packageManager.getPackageInfo(packageName, 0);
- return true;
+ ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0);
+ return appInfo.enabled;
} catch (PackageManager.NameNotFoundException e) {
- Log.d(TAG, "Package " + packageName + " is not installed");
+ Log.d(TAG, "Package " + packageName + " is not installed/enabled");
}
return false;
}
/**
* A BluetoothDevice is exclusively managed if 1) it has field {@link
- * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. 2) the exclusive manager app name is
- * in the allowlist. 3) the exclusive manager app is installed.
+ * BluetoothDevice.METADATA_EXCLUSIVE_MANAGER} in metadata. 2) the exclusive manager app is
+ * installed and enabled.
*/
public static boolean isExclusivelyManagedBluetoothDevice(
@NonNull Context context, @NonNull BluetoothDevice bluetoothDevice) {
- String exclusiveManagerName = getAllowedExclusiveManager(bluetoothDevice);
+ String exclusiveManagerName = getExclusiveManager(bluetoothDevice);
if (exclusiveManagerName == null) {
return false;
}
- if (!isPackageInstalled(context, exclusiveManagerName)) {
+
+ ComponentName exclusiveManagerComponent =
+ ComponentName.unflattenFromString(exclusiveManagerName);
+ String exclusiveManagerPackage = exclusiveManagerComponent != null
+ ? exclusiveManagerComponent.getPackageName() : exclusiveManagerName;
+
+ if (!isPackageInstalledAndEnabled(context, exclusiveManagerPackage)) {
return false;
} else {
- Log.d(TAG, "Found exclusively managed app " + exclusiveManagerName);
+ Log.d(TAG, "Found exclusively managed app " + exclusiveManagerPackage);
return true;
}
}
- /** Return the allowlist for exclusive manager names. */
- @NonNull
- public static Set<String> getExclusiveManagers() {
- return EXCLUSIVE_MANAGERS;
- }
-
/**
* Get CSIP group id for {@link CachedBluetoothDevice}.
*
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index f197f9e..7a2818d 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -28,7 +28,7 @@
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothLeBroadcastReceiveState;
import android.content.Context;
-import android.content.pm.PackageInfo;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
@@ -80,7 +80,8 @@
private static final String CONTROL_METADATA =
"<HEARABLE_CONTROL_SLICE_WITH_WIDTH>" + STRING_METADATA
+ "</HEARABLE_CONTROL_SLICE_WITH_WIDTH>";
- private static final String FAKE_EXCLUSIVE_MANAGER_NAME = "com.fake.name";
+ private static final String TEST_EXCLUSIVE_MANAGER_PACKAGE = "com.test.manager";
+ private static final String TEST_EXCLUSIVE_MANAGER_COMPONENT = "com.test.manager/.component";
@Before
public void setUp() {
@@ -399,7 +400,7 @@
}
@Test
- public void isExclusivelyManagedBluetoothDevice_isNotExclusivelyManaged_returnFalse() {
+ public void isExclusivelyManaged_hasNoManager_returnFalse() {
when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
null);
@@ -408,45 +409,85 @@
}
@Test
- public void isExclusivelyManagedBluetoothDevice_isNotInAllowList_returnFalse() {
+ public void isExclusivelyManaged_hasPackageName_packageNotInstalled_returnFalse()
+ throws Exception {
when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
- FAKE_EXCLUSIVE_MANAGER_NAME.getBytes());
+ TEST_EXCLUSIVE_MANAGER_PACKAGE.getBytes());
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager)
+ .getApplicationInfo(TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
mBluetoothDevice)).isEqualTo(false);
}
@Test
- public void isExclusivelyManagedBluetoothDevice_packageNotInstalled_returnFalse()
+ public void isExclusivelyManaged_hasComponentName_packageNotInstalled_returnFalse()
throws Exception {
- final String exclusiveManagerName =
- BluetoothUtils.getExclusiveManagers().stream().findAny().orElse(
- FAKE_EXCLUSIVE_MANAGER_NAME);
-
when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
- exclusiveManagerName.getBytes());
+ TEST_EXCLUSIVE_MANAGER_COMPONENT.getBytes());
when(mContext.getPackageManager()).thenReturn(mPackageManager);
- doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager).getPackageInfo(
- exclusiveManagerName, 0);
+ doThrow(new PackageManager.NameNotFoundException()).when(mPackageManager)
+ .getApplicationInfo(TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
- mBluetoothDevice)).isEqualTo(false);
+ mBluetoothDevice)).isEqualTo(false);
}
@Test
- public void isExclusivelyManagedBluetoothDevice_isExclusivelyManaged_returnTrue()
- throws Exception {
- final String exclusiveManagerName =
- BluetoothUtils.getExclusiveManagers().stream().findAny().orElse(
- FAKE_EXCLUSIVE_MANAGER_NAME);
-
- when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
- exclusiveManagerName.getBytes());
+ public void isExclusivelyManaged_hasPackageName_packageNotEnabled_returnFalse()
+ throws Exception {
+ ApplicationInfo appInfo = new ApplicationInfo();
+ appInfo.enabled = false;
when(mContext.getPackageManager()).thenReturn(mPackageManager);
- doReturn(new PackageInfo()).when(mPackageManager).getPackageInfo(exclusiveManagerName, 0);
+ doReturn(appInfo).when(mPackageManager).getApplicationInfo(
+ TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+ TEST_EXCLUSIVE_MANAGER_PACKAGE.getBytes());
assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
- mBluetoothDevice)).isEqualTo(true);
+ mBluetoothDevice)).isEqualTo(false);
+ }
+
+ @Test
+ public void isExclusivelyManaged_hasComponentName_packageNotEnabled_returnFalse()
+ throws Exception {
+ ApplicationInfo appInfo = new ApplicationInfo();
+ appInfo.enabled = false;
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ doReturn(appInfo).when(mPackageManager).getApplicationInfo(
+ TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+ TEST_EXCLUSIVE_MANAGER_COMPONENT.getBytes());
+
+ assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+ mBluetoothDevice)).isEqualTo(false);
+ }
+
+ @Test
+ public void isExclusivelyManaged_hasPackageName_packageInstalledAndEnabled_returnTrue()
+ throws Exception {
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+ TEST_EXCLUSIVE_MANAGER_PACKAGE.getBytes());
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ doReturn(new ApplicationInfo()).when(mPackageManager).getApplicationInfo(
+ TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
+
+ assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+ mBluetoothDevice)).isEqualTo(true);
+ }
+
+ @Test
+ public void isExclusivelyManaged_hasComponentName_packageInstalledAndEnabled_returnTrue()
+ throws Exception {
+ when(mBluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)).thenReturn(
+ TEST_EXCLUSIVE_MANAGER_COMPONENT.getBytes());
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ doReturn(new ApplicationInfo()).when(mPackageManager).getApplicationInfo(
+ TEST_EXCLUSIVE_MANAGER_PACKAGE, 0);
+
+ assertThat(BluetoothUtils.isExclusivelyManagedBluetoothDevice(mContext,
+ mBluetoothDevice)).isEqualTo(true);
}
@Test
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 461b6b3..70ce202 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -2949,9 +2949,6 @@
dumpSetting(s, p,
Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ,
SystemSettingsProto.Screen.AUTO_BRIGHTNESS_ADJ);
- dumpSetting(s, p,
- Settings.System.SCREEN_BRIGHTNESS_FLOAT,
- SystemSettingsProto.Screen.BRIGHTNESS_FLOAT);
p.end(screenToken);
dumpSetting(s, p,
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index c891dfc..92167ee 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -935,7 +935,6 @@
Settings.System.VOLUME_VOICE, // deprecated since API 2?
Settings.System.WHEN_TO_MAKE_WIFI_CALLS, // bug?
Settings.System.WINDOW_ORIENTATION_LISTENER_LOG, // used for debugging only
- Settings.System.SCREEN_BRIGHTNESS_FLOAT,
Settings.System.SCREEN_BRIGHTNESS_FOR_ALS,
Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED_DURING_OOBE,
Settings.System.WEAR_TTS_PREWARM_ENABLED,
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 21881f6..80398cd 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -550,6 +550,13 @@
}
flag {
+ name: "enable_contextual_tip_for_mute_volume"
+ namespace: "systemui"
+ description: "Enables the contextual tip for muting the volume."
+ bug: "337737048"
+}
+
+flag {
name: "disable_contextual_tips_frequency_check"
description: "Disables frequency capping check for contextual tips."
namespace: "systemui"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index 1e60b98..d4660fa 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -44,6 +44,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.policy.ScreenDecorationsUtils
import com.android.systemui.Flags.activityTransitionUseLargestWindow
+import java.util.concurrent.Executor
import kotlin.math.roundToInt
private const val TAG = "ActivityTransitionAnimator"
@@ -52,14 +53,19 @@
* A class that allows activities to be started in a seamless way from a view that is transforming
* nicely into the starting window.
*/
-class ActivityTransitionAnimator(
+class ActivityTransitionAnimator
+@JvmOverloads
+constructor(
+ /** The executor that runs on the main thread. */
+ private val mainExecutor: Executor,
+
/** The animator used when animating a View into an app. */
- private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
+ private val transitionAnimator: TransitionAnimator = defaultTransitionAnimator(mainExecutor),
/** The animator used when animating a Dialog into an app. */
// TODO(b/218989950): Remove this animator and instead set the duration of the dim fade out to
// TIMINGS.contentBeforeFadeOutDuration.
- private val dialogToAppAnimator: TransitionAnimator = DEFAULT_DIALOG_TO_APP_ANIMATOR,
+ private val dialogToAppAnimator: TransitionAnimator = defaultDialogToAppAnimator(mainExecutor),
/**
* Whether we should disable the WindowManager timeout. This should be set to true in tests
@@ -100,10 +106,6 @@
// TODO(b/288507023): Remove this flag.
@JvmField val DEBUG_TRANSITION_ANIMATION = Build.IS_DEBUGGABLE
- private val DEFAULT_TRANSITION_ANIMATOR = TransitionAnimator(TIMINGS, INTERPOLATORS)
- private val DEFAULT_DIALOG_TO_APP_ANIMATOR =
- TransitionAnimator(DIALOG_TIMINGS, INTERPOLATORS)
-
/** Durations & interpolators for the navigation bar fading in & out. */
private const val ANIMATION_DURATION_NAV_FADE_IN = 266L
private const val ANIMATION_DURATION_NAV_FADE_OUT = 133L
@@ -121,6 +123,14 @@
* cancelled by WM.
*/
private const val LONG_TRANSITION_TIMEOUT = 5_000L
+
+ private fun defaultTransitionAnimator(mainExecutor: Executor): TransitionAnimator {
+ return TransitionAnimator(mainExecutor, TIMINGS, INTERPOLATORS)
+ }
+
+ private fun defaultDialogToAppAnimator(mainExecutor: Executor): TransitionAnimator {
+ return TransitionAnimator(mainExecutor, DIALOG_TIMINGS, INTERPOLATORS)
+ }
}
/**
@@ -257,9 +267,7 @@
private fun Controller.callOnIntentStartedOnMainThread(willAnimate: Boolean) {
if (Looper.myLooper() != Looper.getMainLooper()) {
- this.transitionContainer.context.mainExecutor.execute {
- callOnIntentStartedOnMainThread(willAnimate)
- }
+ mainExecutor.execute { callOnIntentStartedOnMainThread(willAnimate) }
} else {
if (DEBUG_TRANSITION_ANIMATION) {
Log.d(
@@ -479,12 +487,10 @@
controller: Controller,
callback: Callback,
/** The animator to use to animate the window transition. */
- transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
+ transitionAnimator: TransitionAnimator,
/** Listener for animation lifecycle events. */
listener: Listener? = null
) : IRemoteAnimationRunner.Stub() {
- private val context = controller.transitionContainer.context
-
// This is being passed across IPC boundaries and cycles (through PendingIntentRecords,
// etc.) are possible. So we need to make sure we drop any references that might
// transitively cause leaks when we're done with animation.
@@ -493,11 +499,12 @@
init {
delegate =
AnimationDelegate(
+ mainExecutor,
controller,
callback,
DelegatingAnimationCompletionListener(listener, this::dispose),
transitionAnimator,
- disableWmTimeout
+ disableWmTimeout,
)
}
@@ -510,7 +517,7 @@
finishedCallback: IRemoteAnimationFinishedCallback?
) {
val delegate = delegate
- context.mainExecutor.execute {
+ mainExecutor.execute {
if (delegate == null) {
Log.i(TAG, "onAnimationStart called after completion")
// Animation started too late and timed out already. We need to still
@@ -525,7 +532,7 @@
@BinderThread
override fun onAnimationCancelled() {
val delegate = delegate
- context.mainExecutor.execute {
+ mainExecutor.execute {
delegate ?: Log.wtf(TAG, "onAnimationCancelled called after completion")
delegate?.onAnimationCancelled()
}
@@ -535,19 +542,21 @@
fun dispose() {
// Drop references to animation controller once we're done with the animation
// to avoid leaking.
- context.mainExecutor.execute { delegate = null }
+ mainExecutor.execute { delegate = null }
}
}
class AnimationDelegate
@JvmOverloads
constructor(
+ private val mainExecutor: Executor,
private val controller: Controller,
private val callback: Callback,
/** Listener for animation lifecycle events. */
private val listener: Listener? = null,
/** The animator to use to animate the window transition. */
- private val transitionAnimator: TransitionAnimator = DEFAULT_TRANSITION_ANIMATOR,
+ private val transitionAnimator: TransitionAnimator =
+ defaultTransitionAnimator(mainExecutor),
/**
* Whether we should disable the WindowManager timeout. This should be set to true in tests
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index b89ebfc..f5d01d7 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -37,6 +37,7 @@
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.util.maybeForceFullscreen
import com.android.systemui.util.registerAnimationOnBackInvoked
+import java.util.concurrent.Executor
import kotlin.math.roundToInt
private const val TAG = "DialogTransitionAnimator"
@@ -55,10 +56,16 @@
class DialogTransitionAnimator
@JvmOverloads
constructor(
+ private val mainExecutor: Executor,
private val callback: Callback,
private val interactionJankMonitor: InteractionJankMonitor,
private val featureFlags: AnimationFeatureFlags,
- private val transitionAnimator: TransitionAnimator = TransitionAnimator(TIMINGS, INTERPOLATORS),
+ private val transitionAnimator: TransitionAnimator =
+ TransitionAnimator(
+ mainExecutor,
+ TIMINGS,
+ INTERPOLATORS,
+ ),
private val isForTesting: Boolean = false,
) {
private companion object {
@@ -937,24 +944,9 @@
}
override fun onTransitionAnimationEnd(isExpandingFullyAbove: Boolean) {
- // onLaunchAnimationEnd is called by an Animator at the end of the animation,
- // on a Choreographer animation tick. The following calls will move the animated
- // content from the dialog overlay back to its original position, and this
- // change must be reflected in the next frame given that we then sync the next
- // frame of both the content and dialog ViewRoots. However, in case that content
- // is rendered by Compose, whose compositions are also scheduled on a
- // Choreographer frame, any state change made *right now* won't be reflected in
- // the next frame given that a Choreographer frame can't schedule another and
- // have it happen in the same frame. So we post the forwarded calls to
- // [Controller.onLaunchAnimationEnd], leaving this Choreographer frame, ensuring
- // that the move of the content back to its original window will be reflected in
- // the next frame right after [onLaunchAnimationEnd] is called.
- dialog.context.mainExecutor.execute {
- startController.onTransitionAnimationEnd(isExpandingFullyAbove)
- endController.onTransitionAnimationEnd(isExpandingFullyAbove)
-
- onLaunchAnimationEnd()
- }
+ startController.onTransitionAnimationEnd(isExpandingFullyAbove)
+ endController.onTransitionAnimationEnd(isExpandingFullyAbove)
+ onLaunchAnimationEnd()
}
override fun onTransitionAnimationProgress(
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 679c969..cc55df1 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -31,12 +31,17 @@
import androidx.annotation.VisibleForTesting
import com.android.app.animation.Interpolators.LINEAR
import com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary
+import java.util.concurrent.Executor
import kotlin.math.roundToInt
private const val TAG = "TransitionAnimator"
/** A base class to animate a window (activity or dialog) launch to or return from a view . */
-class TransitionAnimator(private val timings: Timings, private val interpolators: Interpolators) {
+class TransitionAnimator(
+ private val mainExecutor: Executor,
+ private val timings: Timings,
+ private val interpolators: Interpolators,
+) {
companion object {
internal const val DEBUG = false
private val SRC_MODE = PorterDuffXfermode(PorterDuff.Mode.SRC)
@@ -351,11 +356,27 @@
if (DEBUG) {
Log.d(TAG, "Animation ended")
}
- controller.onTransitionAnimationEnd(isExpandingFullyAbove)
- transitionContainerOverlay.remove(windowBackgroundLayer)
- if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
- openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
+ // onAnimationEnd is called at the end of the animation, on a Choreographer
+ // animation tick. During dialog launches, the following calls will move the
+ // animated content from the dialog overlay back to its original position, and
+ // this change must be reflected in the next frame given that we then sync the
+ // next frame of both the content and dialog ViewRoots. During SysUI activity
+ // launches, we will instantly collapse the shade at the end of the transition.
+ // However, if those are rendered by Compose, whose compositions are also
+ // scheduled on a Choreographer frame, any state change made *right now* won't
+ // be reflected in the next frame given that a Choreographer frame can't
+ // schedule another and have it happen in the same frame. So we post the
+ // forwarded calls to [Controller.onLaunchAnimationEnd] in the main executor,
+ // leaving this Choreographer frame, ensuring that any state change applied by
+ // onTransitionAnimationEnd() will be reflected in the same frame.
+ mainExecutor.execute {
+ controller.onTransitionAnimationEnd(isExpandingFullyAbove)
+ transitionContainerOverlay.remove(windowBackgroundLayer)
+
+ if (moveBackgroundLayerWhenAppVisibilityChanges && controller.isLaunching) {
+ openingWindowSyncViewOverlay?.remove(windowBackgroundLayer)
+ }
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt
index 3eb1b14..604b517 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/windowinsets/DisplayCutout.kt
@@ -35,6 +35,7 @@
val viewDisplayCutoutKeyguardStatusBarView: ViewDisplayCutout? = null,
) {
fun width() = abs(right.value - left.value).dp
+ fun height() = abs(bottom.value - top.value).dp
}
enum class CutoutLocation {
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 79b57ca7..3227611 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
@@ -804,6 +804,8 @@
is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(modifier)
is CommunalContentModel.WidgetContent.DisabledWidget ->
DisabledWidgetPlaceholder(model, viewModel, modifier)
+ is CommunalContentModel.WidgetContent.PendingWidget ->
+ PendingWidgetPlaceholder(model, modifier)
is CommunalContentModel.CtaTileInViewMode -> CtaTileInViewModeContent(viewModel, modifier)
is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
is CommunalContentModel.Tutorial -> TutorialContent(modifier)
@@ -929,36 +931,36 @@
Modifier.semantics {
contentDescription = accessibilityLabel
onClick(label = clickActionLabel, action = null)
- val deleteAction =
- CustomAccessibilityAction(removeWidgetActionLabel) {
- contentListState.onRemove(index)
- contentListState.onSaveList()
- true
- }
- val selectWidgetAction =
- CustomAccessibilityAction(clickActionLabel) {
- val currentWidgetKey =
- index?.let {
- keyAtIndexIfEditable(contentListState.list, index)
- }
- viewModel.setSelectedKey(currentWidgetKey)
- true
- }
-
- val actions = mutableListOf(deleteAction, selectWidgetAction)
-
- if (selectedIndex != null && selectedIndex != index) {
- actions.add(
- CustomAccessibilityAction(placeWidgetActionLabel) {
- contentListState.onMove(selectedIndex!!, index)
- contentListState.onSaveList()
- viewModel.setSelectedKey(null)
- true
+ val deleteAction =
+ CustomAccessibilityAction(removeWidgetActionLabel) {
+ contentListState.onRemove(index)
+ contentListState.onSaveList()
+ true
+ }
+ val selectWidgetAction =
+ CustomAccessibilityAction(clickActionLabel) {
+ val currentWidgetKey =
+ index?.let {
+ keyAtIndexIfEditable(contentListState.list, index)
}
- )
+ viewModel.setSelectedKey(currentWidgetKey)
+ true
}
- customActions = actions
+ val actions = mutableListOf(deleteAction, selectWidgetAction)
+
+ if (selectedIndex != null && selectedIndex != index) {
+ actions.add(
+ CustomAccessibilityAction(placeWidgetActionLabel) {
+ contentListState.onMove(selectedIndex!!, index)
+ contentListState.onSaveList()
+ viewModel.setSelectedKey(null)
+ true
+ }
+ )
+ }
+
+ customActions = actions
}
}
) {
@@ -1074,13 +1076,43 @@
Image(
painter = rememberDrawablePainter(icon.loadDrawable(context)),
contentDescription = stringResource(R.string.icon_description_for_disabled_widget),
- modifier = Modifier.size(48.dp),
+ modifier = Modifier.size(Dimensions.IconSize),
colorFilter = ColorFilter.colorMatrix(Colors.DisabledColorFilter),
)
}
}
@Composable
+fun PendingWidgetPlaceholder(
+ model: CommunalContentModel.WidgetContent.PendingWidget,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+ val icon: Icon =
+ if (model.icon != null) {
+ Icon.createWithBitmap(model.icon)
+ } else {
+ Icon.createWithResource(context, android.R.drawable.sym_def_app_icon)
+ }
+
+ Column(
+ modifier =
+ modifier.background(
+ MaterialTheme.colorScheme.surfaceVariant,
+ RoundedCornerShape(dimensionResource(system_app_widget_background_radius))
+ ),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Image(
+ painter = rememberDrawablePainter(icon.loadDrawable(context)),
+ contentDescription = stringResource(R.string.icon_description_for_pending_widget),
+ modifier = Modifier.size(Dimensions.IconSize),
+ )
+ }
+}
+
+@Composable
private fun SmartspaceContent(
model: CommunalContentModel.Smartspace,
modifier: Modifier = Modifier,
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 ec9136d..30b6c6c 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
@@ -36,6 +36,7 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -68,6 +69,8 @@
import com.android.compose.modifiers.thenIf
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
+import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
@@ -152,6 +155,8 @@
modifier: Modifier = Modifier,
shadeSession: SaveableSession,
) {
+ val cutoutLocation = LocalDisplayCutout.current.location
+
val brightnessMirrorShowing by viewModel.brightnessMirrorViewModel.isShowing.collectAsState()
val contentAlpha by
animateFloatAsState(
@@ -183,6 +188,9 @@
// scene (and not the one under it) during a scene transition.
Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
}
+ .thenIf(cutoutLocation != CutoutLocation.CENTER) {
+ Modifier.displayCutoutPadding()
+ },
) {
val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsState()
@@ -320,7 +328,6 @@
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
- modifier = Modifier.padding(horizontal = 16.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 7eaebc2..ff9c5a5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -5,10 +5,13 @@
import com.android.systemui.bouncer.ui.composable.Bouncer
import com.android.systemui.notifications.ui.composable.Notifications
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.CollapseShadeInstantly
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
import com.android.systemui.scene.ui.composable.transitions.bouncerToGoneTransition
import com.android.systemui.scene.ui.composable.transitions.goneToQuickSettingsTransition
import com.android.systemui.scene.ui.composable.transitions.goneToShadeTransition
+import com.android.systemui.scene.ui.composable.transitions.goneToSplitShadeTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToBouncerTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToCommunalTransition
import com.android.systemui.scene.ui.composable.transitions.lockscreenToGoneTransition
@@ -38,6 +41,13 @@
from(
Scenes.Gone,
to = Scenes.Shade,
+ key = GoneToSplitShade,
+ ) {
+ goneToSplitShadeTransition()
+ }
+ from(
+ Scenes.Gone,
+ to = Scenes.Shade,
key = SlightlyFasterShadeCollapse,
) {
goneToShadeTransition(durationScale = 0.9)
@@ -68,5 +78,9 @@
Notifications.Elements.NotificationScrim,
y = { Shade.Dimensions.ScrimOverscrollLimit }
)
+ translate(
+ Shade.Elements.SplitShadeStartColumn,
+ y = { Shade.Dimensions.ScrimOverscrollLimit }
+ )
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
new file mode 100644
index 0000000..4dc36d6
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToSplitShadeTransition.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.ui.composable.transitions
+
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.UserActionDistance
+import com.android.compose.animation.scene.UserActionDistanceScope
+import com.android.systemui.notifications.ui.composable.Notifications
+import com.android.systemui.qs.ui.composable.QuickSettings
+import com.android.systemui.shade.ui.composable.Shade
+import com.android.systemui.shade.ui.composable.ShadeHeader
+import kotlin.time.Duration.Companion.milliseconds
+
+fun TransitionBuilder.goneToSplitShadeTransition(
+ durationScale: Double = 1.0,
+) {
+ spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt())
+ swipeSpec =
+ spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = Shade.Dimensions.ScrimVisibilityThreshold,
+ )
+ distance =
+ object : UserActionDistance {
+ override fun UserActionDistanceScope.absoluteDistance(
+ fromSceneSize: IntSize,
+ orientation: Orientation,
+ ): Float {
+ return fromSceneSize.height.toFloat() * 2 / 3f
+ }
+ }
+
+ fractionRange(end = .33f) { fade(Shade.Elements.BackgroundScrim) }
+
+ fractionRange(start = .33f) {
+ fade(ShadeHeader.Elements.Clock)
+ fade(ShadeHeader.Elements.CollapsedContentStart)
+ fade(ShadeHeader.Elements.CollapsedContentEnd)
+ fade(ShadeHeader.Elements.PrivacyChip)
+ fade(QuickSettings.Elements.SplitShadeQuickSettings)
+ fade(QuickSettings.Elements.FooterActions)
+ fade(Notifications.Elements.NotificationScrim)
+ }
+}
+
+private val DefaultDuration = 500.milliseconds
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 0bd38a1..709a416 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
@@ -50,6 +50,7 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.max
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.LowestZIndexScenePicker
@@ -63,6 +64,7 @@
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
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.privacy.OngoingPrivacyChip
import com.android.systemui.res.R
@@ -77,6 +79,7 @@
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
import com.android.systemui.statusbar.policy.Clock
+import kotlin.math.max
object ShadeHeader {
object Elements {
@@ -121,7 +124,11 @@
}
val cutoutWidth = LocalDisplayCutout.current.width()
+ val cutoutHeight = LocalDisplayCutout.current.height()
+ val cutoutTop = LocalDisplayCutout.current.top
val cutoutLocation = LocalDisplayCutout.current.location
+ val horizontalPadding =
+ max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
val useExpandedFormat by
remember(cutoutLocation) {
@@ -140,7 +147,7 @@
contents =
listOf(
{
- Row {
+ Row(modifier = Modifier.padding(horizontal = horizontalPadding)) {
Clock(
scale = 1f,
viewModel = viewModel,
@@ -157,7 +164,12 @@
},
{
if (isPrivacyChipVisible) {
- Box(modifier = Modifier.height(CollapsedHeight).fillMaxWidth()) {
+ Box(
+ modifier =
+ Modifier.height(CollapsedHeight)
+ .fillMaxWidth()
+ .padding(horizontal = horizontalPadding)
+ ) {
PrivacyChip(
viewModel = viewModel,
modifier = Modifier.align(Alignment.CenterEnd),
@@ -166,9 +178,13 @@
} else {
Row(
horizontalArrangement = Arrangement.End,
- modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
+ modifier =
+ Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
+ .padding(horizontal = horizontalPadding)
) {
- SystemIconContainer {
+ SystemIconContainer(
+ modifier = Modifier.align(Alignment.CenterVertically)
+ ) {
when (LocalWindowSizeClass.current.widthSizeClass) {
WindowWidthSizeClass.Medium,
WindowWidthSizeClass.Expanded ->
@@ -206,7 +222,7 @@
val screenWidth = constraints.maxWidth
val cutoutWidthPx = cutoutWidth.roundToPx()
- val height = CollapsedHeight.roundToPx()
+ val height = max(cutoutHeight + (cutoutTop * 2), CollapsedHeight).roundToPx()
val childConstraints = Constraints.fixed((screenWidth - cutoutWidthPx) / 2, height)
val startMeasurable = measurables[0][0]
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 4ad8b9f..10fe0cab 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
@@ -31,6 +31,7 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -67,6 +68,9 @@
import com.android.compose.modifiers.padding
import com.android.compose.modifiers.thenIf
import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
+import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
+import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.media.controls.ui.composable.MediaCarousel
import com.android.systemui.media.controls.ui.controller.MediaCarouselController
@@ -97,6 +101,7 @@
val MediaCarousel = ElementKey("ShadeMediaCarousel")
val BackgroundScrim =
ElementKey("ShadeBackgroundScrim", scenePicker = LowestZIndexScenePicker)
+ val SplitShadeStartColumn = ElementKey("SplitShadeStartColumn")
}
object Dimensions {
@@ -206,6 +211,8 @@
modifier: Modifier = Modifier,
shadeSession: SaveableSession,
) {
+ val cutoutLocation = LocalDisplayCutout.current.location
+
val maxNotifScrimTop = remember { mutableStateOf(0f) }
val tileSquishiness by
animateSceneFloatAsState(
@@ -241,19 +248,21 @@
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
- Modifier.fillMaxWidth().thenIf(isClickable) {
- Modifier.clickable(onClick = { viewModel.onContentClicked() })
- }
+ Modifier.fillMaxWidth()
+ .thenIf(isClickable) {
+ Modifier.clickable(
+ onClick = { viewModel.onContentClicked() }
+ )
+ }
+ .thenIf(cutoutLocation != CutoutLocation.CENTER) {
+ Modifier.displayCutoutPadding()
+ },
) {
CollapsedShadeHeader(
viewModel = viewModel.shadeHeaderViewModel,
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
- modifier =
- Modifier.padding(
- horizontal = Shade.Dimensions.HorizontalPadding
- )
)
Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) {
QuickSettings(
@@ -312,6 +321,8 @@
modifier: Modifier = Modifier,
shadeSession: SaveableSession,
) {
+ val screenCornerRadius = LocalScreenCornerRadius.current
+
val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
val isCustomizerShowing by viewModel.qsSceneAdapter.isCustomizerShowing.collectAsState()
val customizingAnimationDuration by
@@ -320,7 +331,11 @@
val footerActionsViewModel =
remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
val tileSquishiness by
- animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness)
+ animateSceneFloatAsState(
+ value = 1f,
+ key = QuickSettings.SharedValues.TilesSquishiness,
+ canOverflow = false,
+ )
val unfoldTranslationXForStartSide by
viewModel
.unfoldTranslationX(
@@ -388,8 +403,7 @@
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
modifier =
- Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
- .then(brightnessMirrorShowingModifier)
+ Modifier.then(brightnessMirrorShowingModifier)
.padding(
horizontal = { unfoldTranslationXForStartSide.roundToInt() },
)
@@ -398,9 +412,9 @@
Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
Box(
modifier =
- Modifier.weight(1f).graphicsLayer {
- translationX = unfoldTranslationXForStartSide
- },
+ Modifier.element(Shade.Elements.SplitShadeStartColumn)
+ .weight(1f)
+ .graphicsLayer { translationX = unfoldTranslationXForStartSide },
) {
BrightnessMirror(
viewModel = viewModel.brightnessMirrorViewModel,
@@ -467,7 +481,7 @@
modifier =
Modifier.weight(1f)
.fillMaxHeight()
- .padding(bottom = navBarBottomHeight)
+ .padding(end = screenCornerRadius / 2f, bottom = navBarBottomHeight)
.then(brightnessMirrorShowingModifier)
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index a46f4e5..cb3867f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -34,12 +34,15 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp
import com.android.compose.PlatformSlider
import com.android.compose.PlatformSliderColors
@@ -60,14 +63,31 @@
PlatformSlider(
modifier =
modifier.clearAndSetSemantics {
- if (!state.isEnabled) disabled()
- contentDescription =
- state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
-
- // provide a not animated value to the a11y because it fails to announce the
- // settled value when it changes rapidly.
if (state.isEnabled) {
- progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange)
+ contentDescription = state.label
+ state.a11yClickDescription?.let {
+ customActions =
+ listOf(
+ CustomAccessibilityAction(
+ it,
+ ) {
+ onIconTapped()
+ true
+ }
+ )
+ }
+
+ state.a11yStateDescription?.let { stateDescription = it }
+ ?: run {
+ // provide a not animated value to the a11y because it fails to announce
+ // the settled value when it changes rapidly.
+ progressBarRangeInfo =
+ ProgressBarRangeInfo(state.value, state.valueRange)
+ }
+ } else {
+ disabled()
+ contentDescription =
+ state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
}
setProgress { targetValue ->
val targetDirection =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
index d924d88..92d5c26 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ObservableTransitionState.kt
@@ -74,6 +74,16 @@
*/
val isUserInputOngoing: Flow<Boolean>,
) : ObservableTransitionState
+
+ fun isIdle(scene: SceneKey?): Boolean {
+ return this is Idle && (scene == null || this.currentScene == scene)
+ }
+
+ fun isTransitioning(from: SceneKey? = null, to: SceneKey? = null): Boolean {
+ return this is Transition &&
+ (from == null || this.fromScene == from) &&
+ (to == null || this.toScene == to)
+ }
}
/**
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index f539a23..bdeab79 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -28,16 +28,16 @@
import android.util.AttributeSet
import android.util.MathUtils.constrainedMap
import android.util.TypedValue
-import android.view.View.MeasureSpec.EXACTLY
import android.view.View
+import android.view.View.MeasureSpec.EXACTLY
import android.widget.TextView
import com.android.app.animation.Interpolators
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.animation.GlyphCallback
import com.android.systemui.animation.TextAnimator
import com.android.systemui.customization.R
-import com.android.systemui.log.core.LogcatOnlyMessageBuffer
import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogcatOnlyMessageBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.core.MessageBuffer
import java.io.PrintWriter
@@ -47,11 +47,13 @@
import kotlin.math.min
/**
- * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
- * The time's text color is a gradient that changes its colors based on its controller.
+ * Displays the time with the hour positioned above the minutes (ie: 09 above 30 is 9:30). The
+ * time's text color is a gradient that changes its colors based on its controller.
*/
@SuppressLint("AppCompatCustomView")
-class AnimatableClockView @JvmOverloads constructor(
+class AnimatableClockView
+@JvmOverloads
+constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
@@ -63,7 +65,9 @@
get() = field ?: DEFAULT_LOGGER
var messageBuffer: MessageBuffer
get() = logger.buffer
- set(value) { logger = Logger(value, TAG) }
+ set(value) {
+ logger = Logger(value, TAG)
+ }
var hasCustomPositionUpdatedAnimation: Boolean = false
var migratedClocks: Boolean = false
@@ -77,16 +81,13 @@
private var format: CharSequence? = null
private var descFormat: CharSequence? = null
- @ColorInt
- private var dozingColor = 0
-
- @ColorInt
- private var lockScreenColor = 0
+ @ColorInt private var dozingColor = 0
+ @ColorInt private var lockScreenColor = 0
private var lineSpacingScale = 1f
private val chargeAnimationDelay: Int
private var textAnimator: TextAnimator? = null
- private var onTextAnimatorInitialized: Runnable? = null
+ private var onTextAnimatorInitialized: ((TextAnimator) -> Unit)? = null
private var translateForCenterAnimation = false
private val parentWidth: Int
@@ -94,9 +95,11 @@
// last text size which is not constrained by view height
private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE
- @VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator =
- { layout, invalidateCb ->
- TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb) }
+
+ @VisibleForTesting
+ var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator = { layout, invalidateCb ->
+ TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb)
+ }
// Used by screenshot tests to provide stability
@VisibleForTesting var isAnimationEnabled: Boolean = true
@@ -109,40 +112,55 @@
get() = if (useBoldedVersion()) lockScreenWeightInternal + 100 else lockScreenWeightInternal
/**
- * The number of pixels below the baseline. For fonts that support languages such as
- * Burmese, this space can be significant and should be accounted for when computing layout.
+ * The number of pixels below the baseline. For fonts that support languages such as Burmese,
+ * this space can be significant and should be accounted for when computing layout.
*/
- val bottom get() = paint?.fontMetrics?.bottom ?: 0f
+ val bottom: Float
+ get() = paint?.fontMetrics?.bottom ?: 0f
init {
- val animatableClockViewAttributes = context.obtainStyledAttributes(
- attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes
- )
+ val animatableClockViewAttributes =
+ context.obtainStyledAttributes(
+ attrs,
+ R.styleable.AnimatableClockView,
+ defStyleAttr,
+ defStyleRes
+ )
try {
- dozingWeightInternal = animatableClockViewAttributes.getInt(
- R.styleable.AnimatableClockView_dozeWeight,
- /* default = */ 100
- )
- lockScreenWeightInternal = animatableClockViewAttributes.getInt(
- R.styleable.AnimatableClockView_lockScreenWeight,
- /* default = */ 300
- )
- chargeAnimationDelay = animatableClockViewAttributes.getInt(
- R.styleable.AnimatableClockView_chargeAnimationDelay, /* default = */ 200
- )
+ dozingWeightInternal =
+ animatableClockViewAttributes.getInt(
+ R.styleable.AnimatableClockView_dozeWeight,
+ /* default = */ 100
+ )
+ lockScreenWeightInternal =
+ animatableClockViewAttributes.getInt(
+ R.styleable.AnimatableClockView_lockScreenWeight,
+ /* default = */ 300
+ )
+ chargeAnimationDelay =
+ animatableClockViewAttributes.getInt(
+ R.styleable.AnimatableClockView_chargeAnimationDelay,
+ /* default = */ 200
+ )
} finally {
animatableClockViewAttributes.recycle()
}
- val textViewAttributes = context.obtainStyledAttributes(
- attrs, android.R.styleable.TextView,
- defStyleAttr, defStyleRes
- )
+ val textViewAttributes =
+ context.obtainStyledAttributes(
+ attrs,
+ android.R.styleable.TextView,
+ defStyleAttr,
+ defStyleRes
+ )
try {
- isSingleLineInternal = textViewAttributes.getBoolean(
- android.R.styleable.TextView_singleLine, /* default = */ false)
+ isSingleLineInternal =
+ textViewAttributes.getBoolean(
+ android.R.styleable.TextView_singleLine,
+ /* default = */ false
+ )
} finally {
textViewAttributes.recycle()
}
@@ -156,9 +174,7 @@
refreshFormat()
}
- /**
- * Whether to use a bolded version based on the user specified fontWeightAdjustment.
- */
+ /** Whether to use a bolded version based on the user specified fontWeightAdjustment. */
fun useBoldedVersion(): Boolean {
// "Bold text" fontWeightAdjustment is 300.
return resources.configuration.fontWeightAdjustment > 100
@@ -169,25 +185,30 @@
contentDescription = DateFormat.format(descFormat, time)
val formattedText = DateFormat.format(format, time)
logger.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() }
- // Setting text actually triggers a layout pass (because the text view is set to
- // wrap_content width and TextView always relayouts for this). Avoid needless
- // relayout if the text didn't actually change.
- if (!TextUtils.equals(text, formattedText)) {
- text = formattedText
- logger.d({ "refreshTime: done setting new time text to: $str1" }) {
- str1 = formattedText?.toString()
- }
- // Because the TextLayout may mutate under the hood as a result of the new text, we
- // notify the TextAnimator that it may have changed and request a measure/layout. A
- // crash will occur on the next invocation of setTextStyle if the layout is mutated
- // without being notified TextInterpolator being notified.
- if (layout != null) {
- textAnimator?.updateLayout(layout)
- logger.d("refreshTime: done updating textAnimator layout")
- }
- requestLayout()
- logger.d("refreshTime: after requestLayout")
+
+ // Setting text actually triggers a layout pass in TextView (because the text view is set to
+ // wrap_content width and TextView always relayouts for this). This avoids needless relayout
+ // if the text didn't actually change.
+ if (TextUtils.equals(text, formattedText)) {
+ return
}
+
+ text = formattedText
+ logger.d({ "refreshTime: done setting new time text to: $str1" }) {
+ str1 = formattedText?.toString()
+ }
+
+ // Because the TextLayout may mutate under the hood as a result of the new text, we notify
+ // the TextAnimator that it may have changed and request a measure/layout. A crash will
+ // occur on the next invocation of setTextStyle if the layout is mutated without being
+ // notified TextInterpolator being notified.
+ if (layout != null) {
+ textAnimator?.updateLayout(layout)
+ logger.d("refreshTime: done updating textAnimator layout")
+ }
+
+ requestLayout()
+ logger.d("refreshTime: after requestLayout")
}
fun onTimeZoneChanged(timeZone: TimeZone?) {
@@ -206,19 +227,27 @@
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
logger.d("onMeasure")
- if (migratedClocks && !isSingleLineInternal &&
- MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+
+ if (
+ migratedClocks &&
+ !isSingleLineInternal &&
+ MeasureSpec.getMode(heightMeasureSpec) == EXACTLY
+ ) {
// Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize
- super.setTextSize(TypedValue.COMPLEX_UNIT_PX,
- min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F))
+ super.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX,
+ min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F)
+ )
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val animator = textAnimator
if (animator == null) {
- textAnimator = textAnimatorFactory(layout, ::invalidate)
- onTextAnimatorInitialized?.run()
- onTextAnimatorInitialized = null
+ textAnimator =
+ textAnimatorFactory(layout, ::invalidate)?.also {
+ onTextAnimatorInitialized?.invoke(it)
+ onTextAnimatorInitialized = null
+ }
} else {
animator.updateLayout(layout)
}
@@ -243,15 +272,13 @@
canvas.translate(parentWidth / 4f, 0f)
}
- logger.d({ "onDraw($str1)"}) { str1 = text.toString() }
+ logger.d({ "onDraw($str1)" }) { str1 = text.toString() }
// intentionally doesn't call super.onDraw here or else the text will be rendered twice
textAnimator?.draw(canvas)
canvas.restore()
}
override fun invalidate() {
- @Suppress("UNNECESSARY_SAFE_CALL")
- // logger won't be initialized when called by TextView's constructor
logger.d("invalidate")
super.invalidate()
}
@@ -325,6 +352,7 @@
if (textAnimator == null) {
return
}
+
logger.d("animateFoldAppear")
setTextStyle(
weight = lockScreenWeightInternal,
@@ -348,10 +376,11 @@
}
fun animateCharge(isDozing: () -> Boolean) {
+ // Skip charge animation if dozing animation is already playing.
if (textAnimator == null || textAnimator!!.isRunning()) {
- // Skip charge animation if dozing animation is already playing.
return
}
+
logger.d("animateCharge")
val startAnimPhase2 = Runnable {
setTextStyle(
@@ -409,10 +438,9 @@
/**
* Set text style with an optional animation.
- *
- * By passing -1 to weight, the view preserves its current weight.
- * By passing -1 to textSize, the view preserves its current text size.
- * By passing null to color, the view preserves its current color.
+ * - By passing -1 to weight, the view preserves its current weight.
+ * - By passing -1 to textSize, the view preserves its current text size.
+ * - By passing null to color, the view preserves its current color.
*
* @param weight text weight.
* @param textSize font size.
@@ -428,8 +456,8 @@
delay: Long,
onAnimationEnd: Runnable?
) {
- if (textAnimator != null) {
- textAnimator?.setTextStyle(
+ textAnimator?.let {
+ it.setTextStyle(
weight = weight,
textSize = textSize,
color = color,
@@ -439,23 +467,24 @@
delay = delay,
onAnimationEnd = onAnimationEnd
)
- textAnimator?.glyphFilter = glyphFilter
- } else {
- // when the text animator is set, update its start values
- onTextAnimatorInitialized = Runnable {
- textAnimator?.setTextStyle(
- weight = weight,
- textSize = textSize,
- color = color,
- animate = false,
- duration = duration,
- interpolator = interpolator,
- delay = delay,
- onAnimationEnd = onAnimationEnd
- )
- textAnimator?.glyphFilter = glyphFilter
- }
+ it.glyphFilter = glyphFilter
}
+ ?: run {
+ // when the text animator is set, update its start values
+ onTextAnimatorInitialized = { textAnimator ->
+ textAnimator.setTextStyle(
+ weight = weight,
+ textSize = textSize,
+ color = color,
+ animate = false,
+ duration = duration,
+ interpolator = interpolator,
+ delay = delay,
+ onAnimationEnd = onAnimationEnd
+ )
+ textAnimator.glyphFilter = glyphFilter
+ }
+ }
}
private fun setTextStyle(
@@ -483,12 +512,13 @@
fun refreshFormat(use24HourFormat: Boolean) {
Patterns.update(context)
- format = when {
- isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
- !isSingleLineInternal && use24HourFormat -> DOUBLE_LINE_FORMAT_24_HOUR
- isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
- else -> DOUBLE_LINE_FORMAT_12_HOUR
- }
+ format =
+ when {
+ isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
+ !isSingleLineInternal && use24HourFormat -> DOUBLE_LINE_FORMAT_24_HOUR
+ isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
+ else -> DOUBLE_LINE_FORMAT_12_HOUR
+ }
logger.d({ "refreshFormat($str1)" }) { str1 = format?.toString() }
descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
@@ -510,10 +540,10 @@
pw.println(" time=$time")
}
- private val moveToCenterDelays
+ private val moveToCenterDelays: List<Int>
get() = if (isLayoutRtl) MOVE_LEFT_DELAYS else MOVE_RIGHT_DELAYS
- private val moveToSideDelays
+ private val moveToSideDelays: List<Int>
get() = if (isLayoutRtl) MOVE_RIGHT_DELAYS else MOVE_LEFT_DELAYS
/**
@@ -531,7 +561,7 @@
fun offsetGlyphsForStepClockAnimation(
clockStartLeft: Int,
clockMoveDirection: Int,
- moveFraction: Float
+ moveFraction: Float,
) {
val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0
val currentMoveAmount = left - clockStartLeft
@@ -558,8 +588,8 @@
*
* @param distance is the total distance in pixels to offset the glyphs when animation
* completes. Negative distance means we are animating the position towards the center.
- * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1
- * means it finished moving.
+ * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
+ * it finished moving.
*/
fun offsetGlyphsForStepClockAnimation(
distance: Float,
@@ -568,13 +598,17 @@
for (i in 0 until NUM_DIGITS) {
val dir = if (isLayoutRtl) -1 else 1
val digitFraction =
- getDigitFraction(digit = i, isMovingToCenter = distance > 0, fraction = fraction)
+ getDigitFraction(
+ digit = i,
+ isMovingToCenter = distance > 0,
+ fraction = fraction,
+ )
val moveAmountForDigit = dir * distance * digitFraction
glyphOffsets[i] = moveAmountForDigit
if (distance > 0) {
- // If distance > 0 then we are moving from the left towards the center.
- // We need ensure that the glyphs are offset to the initial position.
+ // If distance > 0 then we are moving from the left towards the center. We need to
+ // ensure that the glyphs are offset to the initial position.
glyphOffsets[i] -= dir * distance
}
}
@@ -582,27 +616,25 @@
}
private fun getDigitFraction(digit: Int, isMovingToCenter: Boolean, fraction: Float): Float {
- // The delay for the digit, in terms of fraction (i.e. the digit should not move
- // during 0.0 - 0.1).
- val digitInitialDelay =
- if (isMovingToCenter) {
- moveToCenterDelays[digit] * MOVE_DIGIT_STEP
- } else {
- moveToSideDelays[digit] * MOVE_DIGIT_STEP
- }
+ // The delay for the digit, in terms of fraction.
+ // (i.e. the digit should not move during 0.0 - 0.1).
+ val delays = if (isMovingToCenter) moveToCenterDelays else moveToSideDelays
+ val digitInitialDelay = delays[digit] * MOVE_DIGIT_STEP
return MOVE_INTERPOLATOR.getInterpolation(
- constrainedMap(
- 0.0f,
- 1.0f,
- digitInitialDelay,
- digitInitialDelay + AVAILABLE_ANIMATION_TIME,
- fraction,
- )
+ constrainedMap(
+ /* rangeMin= */ 0.0f,
+ /* rangeMax= */ 1.0f,
+ /* valueMin= */ digitInitialDelay,
+ /* valueMax= */ digitInitialDelay + AVAILABLE_ANIMATION_TIME,
+ /* value= */ fraction,
)
+ )
}
- // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
- // This is an optimization to ensure we only recompute the patterns when the inputs change.
+ /**
+ * DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often. This
+ * is a cache optimization to ensure we only recompute the patterns when the inputs change.
+ */
private object Patterns {
var sClockView12: String? = null
var sClockView24: String? = null
@@ -610,21 +642,22 @@
fun update(context: Context) {
val locale = Locale.getDefault()
- val res = context.resources
- val clockView12Skel = res.getString(R.string.clock_12hr_format)
- val clockView24Skel = res.getString(R.string.clock_24hr_format)
- val key = locale.toString() + clockView12Skel + clockView24Skel
- if (key == sCacheKey) return
-
- val clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel)
- sClockView12 = clockView12
-
- // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
- // format. The following code removes the AM/PM indicator if we didn't want it.
- if (!clockView12Skel.contains("a")) {
- sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
+ val clockView12Skel = context.resources.getString(R.string.clock_12hr_format)
+ val clockView24Skel = context.resources.getString(R.string.clock_24hr_format)
+ val key = "$locale$clockView12Skel$clockView24Skel"
+ if (key == sCacheKey) {
+ return
}
+ sClockView12 =
+ DateFormat.getBestDateTimePattern(locale, clockView12Skel).let {
+ // CLDR insists on adding an AM/PM indicator even though it wasn't in the format
+ // string. The following code removes the AM/PM indicator if we didn't want it.
+ if (!clockView12Skel.contains("a")) {
+ it.replace("a".toRegex(), "").trim { it <= ' ' }
+ } else it
+ }
+
sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
sCacheKey = key
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
index 3d8159e..9c9ee53 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt
@@ -24,7 +24,6 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.util.settings.FakeSettings
-import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -66,7 +65,7 @@
runCurrent()
- Truth.assertThat(actualValue).isFalse()
+ assertThat(actualValue).isFalse()
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt
new file mode 100644
index 0000000..ca824cb
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/NightDisplayRepositoryTest.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.repository
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import java.time.LocalTime
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayRepositoryTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val testUser = UserHandle.of(1)!!
+ private val testStartTime = LocalTime.MIDNIGHT
+ private val testEndTime = LocalTime.NOON
+ private val colorDisplayManager =
+ mock<ColorDisplayManager> {
+ whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+ whenever(isNightDisplayActivated).thenReturn(false)
+ whenever(nightDisplayCustomStartTime).thenReturn(testStartTime)
+ whenever(nightDisplayCustomEndTime).thenReturn(testEndTime)
+ }
+ private val locationController = FakeLocationController(LeakCheck())
+ private val nightDisplayListener = mock<NightDisplayListener>()
+ private val listenerBuilder =
+ mock<NightDisplayListenerModule.Builder> {
+ whenever(setUser(ArgumentMatchers.anyInt())).thenReturn(this)
+ whenever(build()).thenReturn(nightDisplayListener)
+ }
+ private val globalSettings = kosmos.fakeGlobalSettings
+ private val secureSettings = kosmos.fakeSettings
+ private val testDispatcher = StandardTestDispatcher()
+ private val scope = TestScope(testDispatcher)
+ private val userScopedColorDisplayManager =
+ mock<UserScopedService<ColorDisplayManager>> {
+ whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+ }
+
+ private val underTest =
+ NightDisplayRepository(
+ testDispatcher,
+ scope.backgroundScope,
+ globalSettings,
+ secureSettings,
+ listenerBuilder,
+ userScopedColorDisplayManager,
+ locationController,
+ )
+
+ @Before
+ fun setup() {
+ enrollInForcedNightDisplayAutoMode(INITIALLY_FORCE_AUTO_MODE, testUser)
+ }
+
+ @Test
+ fun nightDisplayState_matchesAutoMode() =
+ scope.runTest {
+ enrollInForcedNightDisplayAutoMode(INITIALLY_FORCE_AUTO_MODE, testUser)
+ val callbackCaptor = argumentCaptor<NightDisplayListener.Callback>()
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+
+ runCurrent()
+
+ verify(nightDisplayListener).setCallback(callbackCaptor.capture())
+ val callback = callbackCaptor.value
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_DISABLED)
+
+ callback.onAutoModeChanged(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+
+ callback.onCustomStartTimeChanged(testStartTime)
+ assertThat(lastState!!.startTime).isEqualTo(testStartTime)
+
+ callback.onCustomEndTimeChanged(testEndTime)
+ assertThat(lastState!!.endTime).isEqualTo(testEndTime)
+
+ callback.onAutoModeChanged(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+ }
+
+ @Test
+ fun nightDisplayState_matchesIsNightDisplayActivated() =
+ scope.runTest {
+ val callbackCaptor = argumentCaptor<NightDisplayListener.Callback>()
+
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ verify(nightDisplayListener).setCallback(callbackCaptor.capture())
+ val callback = callbackCaptor.value
+ assertThat(lastState!!.isActivated)
+ .isEqualTo(colorDisplayManager.isNightDisplayActivated)
+
+ callback.onActivated(true)
+ assertThat(lastState!!.isActivated).isTrue()
+
+ callback.onActivated(false)
+ assertThat(lastState!!.isActivated).isFalse()
+ }
+
+ @Test
+ fun nightDisplayState_matchesController_initiallyCustomAutoMode() =
+ scope.runTest {
+ whenever(colorDisplayManager.nightDisplayAutoMode)
+ .thenReturn(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_CUSTOM_TIME)
+ }
+
+ @Test
+ fun nightDisplayState_matchesController_initiallyTwilightAutoMode() =
+ scope.runTest {
+ whenever(colorDisplayManager.nightDisplayAutoMode)
+ .thenReturn(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ assertThat(lastState!!.autoMode).isEqualTo(ColorDisplayManager.AUTO_MODE_TWILIGHT)
+ }
+
+ @Test
+ fun nightDisplayState_matchesForceAutoMode() =
+ scope.runTest {
+ enrollInForcedNightDisplayAutoMode(false, testUser)
+ val lastState by collectLastValue(underTest.nightDisplayState(testUser))
+ runCurrent()
+
+ assertThat(lastState!!.shouldForceAutoMode).isEqualTo(false)
+
+ enrollInForcedNightDisplayAutoMode(true, testUser)
+ assertThat(lastState!!.shouldForceAutoMode).isEqualTo(true)
+ }
+
+ private fun enrollInForcedNightDisplayAutoMode(enroll: Boolean, userHandle: UserHandle) {
+ globalSettings.putString(
+ Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE,
+ if (enroll) NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+ else NIGHT_DISPLAY_FORCED_AUTO_MODE_UNAVAILABLE
+ )
+ secureSettings.putIntForUser(
+ Settings.Secure.NIGHT_DISPLAY_AUTO_MODE,
+ if (enroll) NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET else NIGHT_DISPLAY_AUTO_MODE_RAW_SET,
+ userHandle.identifier
+ )
+ }
+
+ private companion object {
+ const val INITIALLY_FORCE_AUTO_MODE = false
+ const val NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE = "1"
+ const val NIGHT_DISPLAY_FORCED_AUTO_MODE_UNAVAILABLE = "0"
+ const val NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET = -1
+ const val NIGHT_DISPLAY_AUTO_MODE_RAW_SET = 0
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
index 2386957..7628deb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageChangeRepositoryTest.kt
@@ -50,6 +50,7 @@
@Mock private lateinit var context: Context
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var handler: Handler
+ @Mock private lateinit var packageInstallerMonitor: PackageInstallerMonitor
private lateinit var repository: PackageChangeRepository
private lateinit var updateMonitor: PackageUpdateMonitor
@@ -60,19 +61,20 @@
MockitoAnnotations.initMocks(this@PackageChangeRepositoryTest)
whenever(context.packageManager).thenReturn(packageManager)
- repository = PackageChangeRepositoryImpl { user ->
- updateMonitor =
- PackageUpdateMonitor(
- user = user,
- bgDispatcher = testDispatcher,
- scope = applicationCoroutineScope,
- context = context,
- bgHandler = handler,
- logger = PackageUpdateLogger(logcatLogBuffer()),
- systemClock = fakeSystemClock,
- )
- updateMonitor
- }
+ repository =
+ PackageChangeRepositoryImpl(packageInstallerMonitor) { user ->
+ updateMonitor =
+ PackageUpdateMonitor(
+ user = user,
+ bgDispatcher = testDispatcher,
+ scope = applicationCoroutineScope,
+ context = context,
+ bgHandler = handler,
+ logger = PackageUpdateLogger(logcatLogBuffer()),
+ systemClock = fakeSystemClock,
+ )
+ updateMonitor
+ }
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
new file mode 100644
index 0000000..5556b04
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
@@ -0,0 +1,228 @@
+/*
+ * 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.common.data.repository
+
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageInstaller.SessionInfo
+import android.graphics.Bitmap
+import android.os.fakeExecutorHandler
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.PackageInstallSession
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+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.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PackageInstallerMonitorTest : SysuiTestCase() {
+ @Mock private lateinit var packageInstaller: PackageInstaller
+ @Mock private lateinit var icon1: Bitmap
+ @Mock private lateinit var icon2: Bitmap
+ @Mock private lateinit var icon3: Bitmap
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+
+ private val handler = kosmos.fakeExecutorHandler
+
+ private lateinit var session1: SessionInfo
+ private lateinit var session2: SessionInfo
+ private lateinit var session3: SessionInfo
+
+ private lateinit var defaultSessions: List<SessionInfo>
+
+ private lateinit var underTest: PackageInstallerMonitor
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ session1 =
+ SessionInfo().apply {
+ sessionId = 1
+ appPackageName = "pkg_name_1"
+ appIcon = icon1
+ }
+ session2 =
+ SessionInfo().apply {
+ sessionId = 2
+ appPackageName = "pkg_name_2"
+ appIcon = icon2
+ }
+ session3 =
+ SessionInfo().apply {
+ sessionId = 3
+ appPackageName = "pkg_name_3"
+ appIcon = icon3
+ }
+ defaultSessions = listOf(session1, session2)
+
+ whenever(packageInstaller.allSessions).thenReturn(defaultSessions)
+ whenever(packageInstaller.getSessionInfo(1)).thenReturn(session1)
+ whenever(packageInstaller.getSessionInfo(2)).thenReturn(session2)
+
+ underTest =
+ PackageInstallerMonitor(
+ handler,
+ kosmos.applicationCoroutineScope,
+ logcatLogBuffer("PackageInstallerRepositoryImplTest"),
+ packageInstaller,
+ )
+ }
+
+ @Test
+ fun installSessions_callbacksRegisteredOnlyWhenFlowIsCollected() =
+ testScope.runTest {
+ // Verify callback not added before flow is collected
+ verify(packageInstaller, never()).registerSessionCallback(any(), eq(handler))
+
+ // Start collecting the flow
+ val job =
+ backgroundScope.launch {
+ underTest.installSessionsForPrimaryUser.collect {
+ // Do nothing with the value
+ }
+ }
+ runCurrent()
+
+ // Verify callback added only after flow is collected
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // Verify callback not removed
+ verify(packageInstaller, never()).unregisterSessionCallback(any())
+
+ // Stop collecting the flow
+ job.cancel()
+ runCurrent()
+
+ // Verify callback removed once flow stops being collected
+ verify(packageInstaller).unregisterSessionCallback(eq(callback))
+ }
+
+ @Test
+ fun installSessions_newSessionsAreAdded() =
+ testScope.runTest {
+ val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // New session added
+ whenever(packageInstaller.getSessionInfo(3)).thenReturn(session3)
+ callback.onCreated(3)
+ runCurrent()
+
+ // Verify flow updated with the new session
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions + session3)
+ }
+
+ @Test
+ fun installSessions_finishedSessionsAreRemoved() =
+ testScope.runTest {
+ val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // Session 1 finished successfully
+ callback.onFinished(1, /* success = */ true)
+ runCurrent()
+
+ // Verify flow updated with session 1 removed
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions - session1)
+ }
+
+ @Test
+ fun installSessions_sessionsUpdatedOnBadgingChange() =
+ testScope.runTest {
+ val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // App icon for session 1 updated
+ val newSession =
+ SessionInfo().apply {
+ sessionId = 1
+ appPackageName = "pkg_name_1"
+ appIcon = mock()
+ }
+ whenever(packageInstaller.getSessionInfo(1)).thenReturn(newSession)
+ callback.onBadgingChanged(1)
+ runCurrent()
+
+ // Verify flow updated with the new session 1
+ assertThat(installSessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions - session1 + newSession)
+ }
+
+ private val represents =
+ Correspondence.from<PackageInstallSession, SessionInfo>(
+ { actual, expected ->
+ actual?.sessionId == expected?.sessionId &&
+ actual?.packageName == expected?.appPackageName &&
+ actual?.icon == expected?.getAppIcon()
+ },
+ "represents",
+ )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index fe4d32d..6ce6cdb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -17,16 +17,18 @@
package com.android.systemui.communal.data.repository
import android.app.backup.BackupManager
-import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
import android.content.ComponentName
import android.content.applicationContext
+import android.graphics.Bitmap
import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.repository.fakePackageChangeRepository
+import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.communal.data.backup.CommunalBackupUtils
import com.android.systemui.communal.data.db.CommunalItemRank
import com.android.systemui.communal.data.db.CommunalWidgetDao
@@ -46,10 +48,10 @@
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
-import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
@@ -58,7 +60,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.anyInt
import org.mockito.Mockito.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
@@ -68,10 +69,10 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
- @Mock private lateinit var appWidgetManager: AppWidgetManager
@Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
- @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo
@Mock private lateinit var providerInfoA: AppWidgetProviderInfo
+ @Mock private lateinit var providerInfoB: AppWidgetProviderInfo
+ @Mock private lateinit var providerInfoC: AppWidgetProviderInfo
@Mock private lateinit var communalWidgetHost: CommunalWidgetHost
@Mock private lateinit var communalWidgetDao: CommunalWidgetDao
@Mock private lateinit var backupManager: BackupManager
@@ -79,9 +80,11 @@
private lateinit var backupUtils: CommunalBackupUtils
private lateinit var logBuffer: LogBuffer
private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>>
+ private lateinit var fakeProviders: MutableStateFlow<Map<Int, AppWidgetProviderInfo?>>
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
+ private val packageChangeRepository = kosmos.fakePackageChangeRepository
private val fakeAllowlist =
listOf(
@@ -96,6 +99,7 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
fakeWidgets = MutableStateFlow(emptyMap())
+ fakeProviders = MutableStateFlow(emptyMap())
logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest")
backupUtils = CommunalBackupUtils(kosmos.applicationContext)
@@ -103,12 +107,11 @@
overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray())
- whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch")
whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets)
+ whenever(communalWidgetHost.appWidgetProviders).thenReturn(fakeProviders)
underTest =
CommunalWidgetRepositoryImpl(
- Optional.of(appWidgetManager),
appWidgetHost,
testScope.backgroundScope,
kosmos.testDispatcher,
@@ -117,6 +120,7 @@
logBuffer,
backupManager,
backupUtils,
+ packageChangeRepository,
)
}
@@ -126,15 +130,13 @@
val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1)
val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L)
fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry)
- whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA)
-
- installedProviders(listOf(stopwatchProviderInfo))
+ fakeProviders.value = mapOf(1 to providerInfoA)
val communalWidgets by collectLastValue(underTest.communalWidgets)
verify(communalWidgetDao).getWidgets()
assertThat(communalWidgets)
.containsExactly(
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = communalWidgetItemEntry.widgetId,
providerInfo = providerInfoA,
priority = communalItemRankEntry.rank,
@@ -146,6 +148,102 @@
}
@Test
+ fun communalWidgets_widgetsWithoutMatchingProvidersAreSkipped() =
+ testScope.runTest {
+ // Set up 4 widgets, but widget 3 and 4 don't have matching providers
+ fakeWidgets.value =
+ mapOf(
+ CommunalItemRank(uid = 1L, rank = 1) to
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+ CommunalItemRank(uid = 2L, rank = 2) to
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+ CommunalItemRank(uid = 3L, rank = 3) to
+ CommunalWidgetItem(uid = 3L, 3, "pk_3/cls_3", 3L),
+ CommunalItemRank(uid = 4L, rank = 4) to
+ CommunalWidgetItem(uid = 4L, 4, "pk_4/cls_4", 4L),
+ )
+ fakeProviders.value =
+ mapOf(
+ 1 to providerInfoA,
+ 2 to providerInfoB,
+ )
+
+ // Expect to see only widget 1 and 2
+ val communalWidgets by collectLastValue(underTest.communalWidgets)
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 1,
+ providerInfo = providerInfoA,
+ priority = 1,
+ ),
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 2,
+ providerInfo = providerInfoB,
+ priority = 2,
+ ),
+ )
+ }
+
+ @Test
+ fun communalWidgets_updatedWhenProvidersUpdate() =
+ testScope.runTest {
+ // Set up widgets and providers
+ fakeWidgets.value =
+ mapOf(
+ CommunalItemRank(uid = 1L, rank = 1) to
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+ CommunalItemRank(uid = 2L, rank = 2) to
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+ )
+ fakeProviders.value =
+ mapOf(
+ 1 to providerInfoA,
+ 2 to providerInfoB,
+ )
+
+ // Expect two widgets
+ val communalWidgets by collectLastValue(underTest.communalWidgets)
+ assertThat(communalWidgets).isNotNull()
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 1,
+ providerInfo = providerInfoA,
+ priority = 1,
+ ),
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 2,
+ providerInfo = providerInfoB,
+ priority = 2,
+ ),
+ )
+
+ // Provider info updated for widget 1
+ fakeProviders.value =
+ mapOf(
+ 1 to providerInfoC,
+ 2 to providerInfoB,
+ )
+ runCurrent()
+
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 1,
+ // Verify that provider info updated
+ providerInfo = providerInfoC,
+ priority = 1,
+ ),
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 2,
+ providerInfo = providerInfoB,
+ priority = 2,
+ ),
+ )
+ }
+
+ @Test
fun addWidget_allocateId_bindWidget_andAddToDb() =
testScope.runTest {
val provider = ComponentName("pkg_name", "cls_name")
@@ -434,9 +532,102 @@
assertThat(restoredWidget2.rank).isEqualTo(expectedWidget2.rank)
}
- private fun installedProviders(providers: List<AppWidgetProviderInfo>) {
- whenever(appWidgetManager.installedProviders).thenReturn(providers)
- }
+ @Test
+ fun pendingWidgets() =
+ testScope.runTest {
+ fakeWidgets.value =
+ mapOf(
+ CommunalItemRank(uid = 1L, rank = 1) to
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+ CommunalItemRank(uid = 2L, rank = 2) to
+ CommunalWidgetItem(uid = 2L, 2, "pk_2/cls_2", 2L),
+ )
+
+ // Widget 1 is installed
+ fakeProviders.value = mapOf(1 to providerInfoA)
+
+ // Widget 2 is pending install
+ val fakeIcon = mock<Bitmap>()
+ packageChangeRepository.setInstallSessions(
+ listOf(
+ PackageInstallSession(
+ sessionId = 1,
+ packageName = "pk_2",
+ icon = fakeIcon,
+ user = UserHandle.CURRENT,
+ )
+ )
+ )
+
+ val communalWidgets by collectLastValue(underTest.communalWidgets)
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 1,
+ providerInfo = providerInfoA,
+ priority = 1,
+ ),
+ CommunalWidgetContentModel.Pending(
+ appWidgetId = 2,
+ priority = 2,
+ packageName = "pk_2",
+ icon = fakeIcon,
+ user = UserHandle.CURRENT,
+ ),
+ )
+ }
+
+ @Test
+ fun pendingWidgets_pendingWidgetBecomesAvailableAfterInstall() =
+ testScope.runTest {
+ fakeWidgets.value =
+ mapOf(
+ CommunalItemRank(uid = 1L, rank = 1) to
+ CommunalWidgetItem(uid = 1L, 1, "pk_1/cls_1", 1L),
+ )
+
+ // Widget 1 is pending install
+ val fakeIcon = mock<Bitmap>()
+ packageChangeRepository.setInstallSessions(
+ listOf(
+ PackageInstallSession(
+ sessionId = 1,
+ packageName = "pk_1",
+ icon = fakeIcon,
+ user = UserHandle.CURRENT,
+ )
+ )
+ )
+
+ val communalWidgets by collectLastValue(underTest.communalWidgets)
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Pending(
+ appWidgetId = 1,
+ priority = 1,
+ packageName = "pk_1",
+ icon = fakeIcon,
+ user = UserHandle.CURRENT,
+ ),
+ )
+
+ // Package for widget 1 finished installing
+ packageChangeRepository.setInstallSessions(emptyList())
+
+ // Provider info for widget 1 becomes available
+ fakeProviders.value = mapOf(1 to providerInfoA)
+
+ runCurrent()
+
+ assertThat(communalWidgets)
+ .containsExactly(
+ CommunalWidgetContentModel.Available(
+ appWidgetId = 1,
+ providerInfo = providerInfoA,
+ priority = 1,
+ ),
+ )
+ }
private fun setAppWidgetIds(ids: List<Int>) {
whenever(appWidgetHost.appWidgetIds).thenReturn(ids.toIntArray())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 456fb79..766798c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -23,6 +23,7 @@
import android.appwidget.AppWidgetProviderInfo
import android.content.Intent
import android.content.pm.UserInfo
+import android.graphics.Bitmap
import android.os.UserHandle
import android.os.UserManager
import android.os.userManager
@@ -871,7 +872,14 @@
// One widget is filtered out and the remaining two link to main user id.
assertThat(checkNotNull(widgetContent).size).isEqualTo(2)
widgetContent!!.forEachIndexed { _, model ->
- assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id)
+ assertThat(model is CommunalContentModel.WidgetContent.Widget).isTrue()
+ assertThat(
+ (model as CommunalContentModel.WidgetContent.Widget)
+ .providerInfo
+ .profile
+ ?.identifier
+ )
+ .isEqualTo(MAIN_USER_INFO.id)
}
}
@@ -1037,9 +1045,9 @@
runCurrent()
val widgetContent by collectLastValue(underTest.widgetContent)
- // Given three widgets, and one of them is associated with work profile.
+ // One available work widget, one pending work widget, and one regular available widget.
val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+ val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id)
val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
val widgets = listOf(widget1, widget2, widget3)
widgetRepository.setCommunalWidgets(widgets)
@@ -1049,11 +1057,9 @@
DevicePolicyManager.KEYGUARD_DISABLE_WIDGETS_ALL
)
- // Widget under work profile is filtered out and the remaining two link to main user id.
- assertThat(widgetContent).hasSize(2)
- widgetContent!!.forEach { model ->
- assertThat(model.providerInfo.profile?.identifier).isEqualTo(MAIN_USER_INFO.id)
- }
+ // Widgets under work profile are filtered out. Only the regular widget remains.
+ assertThat(widgetContent).hasSize(1)
+ assertThat(widgetContent?.get(0)?.appWidgetId).isEqualTo(3)
}
@Test
@@ -1076,7 +1082,7 @@
val widgetContent by collectLastValue(underTest.widgetContent)
// Given three widgets, and one of them is associated with work profile.
val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+ val widget2 = createPendingWidgetForUser(2, userId = USER_INFO_WORK.id)
val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
val widgets = listOf(widget1, widget2, widget3)
widgetRepository.setCommunalWidgets(widgets)
@@ -1086,10 +1092,11 @@
DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE
)
- // Widget under work profile is available.
+ // Widgets under work profile are available.
assertThat(widgetContent).hasSize(3)
- assertThat(widgetContent!![0].providerInfo.profile?.identifier)
- .isEqualTo(USER_INFO_WORK.id)
+ assertThat(widgetContent?.get(0)?.appWidgetId).isEqualTo(1)
+ assertThat(widgetContent?.get(1)?.appWidgetId).isEqualTo(2)
+ assertThat(widgetContent?.get(2)?.appWidgetId).isEqualTo(3)
}
@Test
@@ -1182,8 +1189,11 @@
)
}
- private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
- mock<CommunalWidgetContentModel> {
+ private fun createWidgetForUser(
+ appWidgetId: Int,
+ userId: Int
+ ): CommunalWidgetContentModel.Available =
+ mock<CommunalWidgetContentModel.Available> {
whenever(this.appWidgetId).thenReturn(appWidgetId)
val providerInfo =
mock<AppWidgetProviderInfo>().apply {
@@ -1193,11 +1203,27 @@
whenever(this.providerInfo).thenReturn(providerInfo)
}
+ private fun createPendingWidgetForUser(
+ appWidgetId: Int,
+ priority: Int = 0,
+ packageName: String = "",
+ icon: Bitmap? = null,
+ userId: Int = 0,
+ ): CommunalWidgetContentModel.Pending {
+ return CommunalWidgetContentModel.Pending(
+ appWidgetId = appWidgetId,
+ priority = priority,
+ packageName = packageName,
+ icon = icon,
+ user = UserHandle(userId),
+ )
+ }
+
private fun createWidgetWithCategory(
appWidgetId: Int,
category: Int
): CommunalWidgetContentModel =
- mock<CommunalWidgetContentModel> {
+ mock<CommunalWidgetContentModel.Available> {
whenever(this.appWidgetId).thenReturn(appWidgetId)
val providerInfo = mock<AppWidgetProviderInfo>().apply { widgetCategory = category }
whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
index 02d927a..f9d5073 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModelTest.kt
@@ -86,4 +86,26 @@
)
assertThat(isUmoOnCommunal).isFalse()
}
+
+ @Test
+ fun testIsUmoOnCommunalDuringTransitionBetweenOccludedAndGlanceableHub() =
+ testScope.runTest {
+ val isUmoOnCommunal by collectLastValue(underTest.isUmoOnCommunal)
+ assertThat(isUmoOnCommunal).isNull()
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.GLANCEABLE_HUB,
+ testScope
+ )
+ assertThat(isUmoOnCommunal).isTrue()
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GLANCEABLE_HUB,
+ to = KeyguardState.OCCLUDED,
+ testScope
+ )
+
+ assertThat(isUmoOnCommunal).isFalse()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
index 89a4c04..b3a12a6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt
@@ -28,6 +28,8 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -36,6 +38,9 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -96,4 +101,137 @@
assertThat(appWidgetIdToRemove).isEqualTo(2)
}
+
+ @Test
+ fun observer_onHostStartListeningTriggeredWhileObserverActive() =
+ testScope.runTest {
+ // Observer added
+ val observer = mock<CommunalAppWidgetHost.Observer>()
+ underTest.addObserver(observer)
+ runCurrent()
+
+ // Verify callback triggered
+ verify(observer, never()).onHostStartListening()
+ underTest.startListening()
+ runCurrent()
+ verify(observer).onHostStartListening()
+
+ clearInvocations(observer)
+
+ // Observer removed
+ underTest.removeObserver(observer)
+ runCurrent()
+
+ // Verify callback not triggered
+ underTest.startListening()
+ runCurrent()
+ verify(observer, never()).onHostStartListening()
+ }
+
+ @Test
+ fun observer_onHostStopListeningTriggeredWhileObserverActive() =
+ testScope.runTest {
+ // Observer added
+ val observer = mock<CommunalAppWidgetHost.Observer>()
+ underTest.addObserver(observer)
+ runCurrent()
+
+ // Verify callback triggered
+ verify(observer, never()).onHostStopListening()
+ underTest.stopListening()
+ runCurrent()
+ verify(observer).onHostStopListening()
+
+ clearInvocations(observer)
+
+ // Observer removed
+ underTest.removeObserver(observer)
+ runCurrent()
+
+ // Verify callback not triggered
+ underTest.stopListening()
+ runCurrent()
+ verify(observer, never()).onHostStopListening()
+ }
+
+ @Test
+ fun observer_onAllocateAppWidgetIdTriggeredWhileObserverActive() =
+ testScope.runTest {
+ // Observer added
+ val observer = mock<CommunalAppWidgetHost.Observer>()
+ underTest.addObserver(observer)
+ runCurrent()
+
+ // Verify callback triggered
+ verify(observer, never()).onAllocateAppWidgetId(any())
+ val id = underTest.allocateAppWidgetId()
+ runCurrent()
+ verify(observer).onAllocateAppWidgetId(eq(id))
+
+ clearInvocations(observer)
+
+ // Observer removed
+ underTest.removeObserver(observer)
+ runCurrent()
+
+ // Verify callback not triggered
+ underTest.allocateAppWidgetId()
+ runCurrent()
+ verify(observer, never()).onAllocateAppWidgetId(any())
+ }
+
+ @Test
+ fun observer_onDeleteAppWidgetIdTriggeredWhileObserverActive() =
+ testScope.runTest {
+ // Observer added
+ val observer = mock<CommunalAppWidgetHost.Observer>()
+ underTest.addObserver(observer)
+ runCurrent()
+
+ // Verify callback triggered
+ verify(observer, never()).onDeleteAppWidgetId(any())
+ underTest.deleteAppWidgetId(1)
+ runCurrent()
+ verify(observer).onDeleteAppWidgetId(eq(1))
+
+ clearInvocations(observer)
+
+ // Observer removed
+ underTest.removeObserver(observer)
+ runCurrent()
+
+ // Verify callback not triggered
+ underTest.deleteAppWidgetId(2)
+ runCurrent()
+ verify(observer, never()).onDeleteAppWidgetId(any())
+ }
+
+ @Test
+ fun observer_multipleObservers() =
+ testScope.runTest {
+ // Set up two observers
+ val observer1 = mock<CommunalAppWidgetHost.Observer>()
+ val observer2 = mock<CommunalAppWidgetHost.Observer>()
+ underTest.addObserver(observer1)
+ underTest.addObserver(observer2)
+ runCurrent()
+
+ // Verify both observers triggered
+ verify(observer1, never()).onHostStartListening()
+ verify(observer2, never()).onHostStartListening()
+ underTest.startListening()
+ runCurrent()
+ verify(observer1).onHostStartListening()
+ verify(observer2).onHostStartListening()
+
+ // Observer 1 removed
+ underTest.removeObserver(observer1)
+ runCurrent()
+
+ // Verify only observer 2 is triggered
+ underTest.stopListening()
+ runCurrent()
+ verify(observer2).onHostStopListening()
+ verify(observer1, never()).onHostStopListening()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 9aebc30..6ca04df 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -123,12 +123,12 @@
// Widgets available.
val widgets =
listOf(
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 0,
priority = 30,
providerInfo = providerInfo,
),
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 1,
priority = 20,
providerInfo = providerInfo,
@@ -177,12 +177,12 @@
// Widgets available.
val widgets =
listOf(
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 0,
priority = 30,
providerInfo = providerInfo,
),
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 1,
priority = 20,
providerInfo = providerInfo,
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 569116c..1f8cb8a 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
@@ -90,7 +90,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class CommunalViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
@Mock private lateinit var user: UserInfo
@Mock private lateinit var providerInfo: AppWidgetProviderInfo
@@ -111,7 +111,7 @@
private lateinit var underTest: CommunalViewModel
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
@@ -186,12 +186,12 @@
// Widgets available.
val widgets =
listOf(
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 0,
priority = 30,
providerInfo = providerInfo,
),
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 1,
priority = 20,
providerInfo = providerInfo,
@@ -245,7 +245,7 @@
widgetRepository.setCommunalWidgets(
listOf(
- CommunalWidgetContentModel(
+ CommunalWidgetContentModel.Available(
appWidgetId = 1,
priority = 1,
providerInfo = providerInfo,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index 6cae5d3..3d2eabf 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -18,6 +18,7 @@
import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
+import android.graphics.Bitmap
import android.os.UserHandle
import android.provider.Settings
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -60,6 +61,7 @@
private val kosmos = testKosmos()
@Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
+ @Mock private lateinit var communalWidgetHost: CommunalWidgetHost
private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int>
@@ -78,6 +80,7 @@
underTest =
CommunalAppWidgetHostStartable(
appWidgetHost,
+ communalWidgetHost,
kosmos.communalInteractor,
kosmos.fakeUserTracker,
kosmos.applicationCoroutineScope,
@@ -143,16 +146,44 @@
}
@Test
+ fun observeHostWhenCommunalIsAvailable() =
+ with(kosmos) {
+ testScope.runTest {
+ setCommunalAvailable(true)
+ communalInteractor.setEditModeOpen(false)
+ verify(communalWidgetHost, never()).startObservingHost()
+ verify(communalWidgetHost, never()).stopObservingHost()
+
+ underTest.start()
+ runCurrent()
+
+ verify(communalWidgetHost).startObservingHost()
+ verify(communalWidgetHost, never()).stopObservingHost()
+
+ setCommunalAvailable(false)
+ runCurrent()
+
+ verify(communalWidgetHost).stopObservingHost()
+ }
+ }
+
+ @Test
fun removeAppWidgetReportedByHost() =
with(kosmos) {
testScope.runTest {
// Set up communal widgets
val widget1 =
- mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(1) }
+ mock<CommunalWidgetContentModel.Available> {
+ whenever(this.appWidgetId).thenReturn(1)
+ }
val widget2 =
- mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(2) }
+ mock<CommunalWidgetContentModel.Available> {
+ whenever(this.appWidgetId).thenReturn(2)
+ }
val widget3 =
- mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(3) }
+ mock<CommunalWidgetContentModel.Available> {
+ whenever(this.appWidgetId).thenReturn(3)
+ }
fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3))
underTest.start()
@@ -184,8 +215,9 @@
userInfos = listOf(MAIN_USER_INFO, USER_INFO_WORK),
selectedUserIndex = 0,
)
+ // One work widget, one pending work widget, and one personal widget.
val widget1 = createWidgetForUser(1, USER_INFO_WORK.id)
- val widget2 = createWidgetForUser(2, MAIN_USER_INFO.id)
+ val widget2 = createPendingWidgetForUser(2, USER_INFO_WORK.id)
val widget3 = createWidgetForUser(3, MAIN_USER_INFO.id)
val widgets = listOf(widget1, widget2, widget3)
fakeCommunalWidgetRepository.setCommunalWidgets(widgets)
@@ -209,8 +241,8 @@
fakeKeyguardRepository.setKeyguardShowing(true)
runCurrent()
- // Widget created for work profile is removed.
- assertThat(communalWidgets).containsExactly(widget2, widget3)
+ // Both work widgets are removed.
+ assertThat(communalWidgets).containsExactly(widget3)
}
}
@@ -227,14 +259,32 @@
)
}
- private fun createWidgetForUser(appWidgetId: Int, userId: Int): CommunalWidgetContentModel =
- mock<CommunalWidgetContentModel> {
+ private fun createWidgetForUser(
+ appWidgetId: Int,
+ userId: Int
+ ): CommunalWidgetContentModel.Available =
+ mock<CommunalWidgetContentModel.Available> {
whenever(this.appWidgetId).thenReturn(appWidgetId)
val providerInfo = mock<AppWidgetProviderInfo>()
whenever(providerInfo.profile).thenReturn(UserHandle(userId))
whenever(this.providerInfo).thenReturn(providerInfo)
}
+ private fun createPendingWidgetForUser(
+ appWidgetId: Int,
+ userId: Int,
+ priority: Int = 0,
+ packageName: String = "",
+ icon: Bitmap? = null,
+ ): CommunalWidgetContentModel.Pending =
+ CommunalWidgetContentModel.Pending(
+ appWidgetId = appWidgetId,
+ priority = priority,
+ packageName = packageName,
+ icon = icon,
+ user = UserHandle(userId),
+ )
+
private companion object {
val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt
index 88f5e1b..054e516 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.communal.widgets
+import android.appwidget.AppWidgetHost
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
@@ -26,6 +27,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
@@ -40,6 +43,7 @@
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -47,6 +51,8 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -59,6 +65,10 @@
@Mock private lateinit var appWidgetManager: AppWidgetManager
@Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
+ @Mock private lateinit var providerInfo1: AppWidgetProviderInfo
+ @Mock private lateinit var providerInfo2: AppWidgetProviderInfo
+ @Mock private lateinit var providerInfo3: AppWidgetProviderInfo
+
private val selectedUserInteractor: SelectedUserInteractor by lazy {
kosmos.selectedUserInteractor
}
@@ -69,8 +79,19 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
+ whenever(
+ appWidgetManager.bindAppWidgetIdIfAllowed(
+ any<Int>(),
+ any<UserHandle>(),
+ any<ComponentName>(),
+ any<Bundle>()
+ )
+ )
+ .thenReturn(true)
+
underTest =
CommunalWidgetHost(
+ kosmos.applicationCoroutineScope,
Optional.of(appWidgetManager),
appWidgetHost,
selectedUserInteractor,
@@ -89,15 +110,6 @@
val user = UserHandle(checkNotNull(userId))
whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(widgetId)
- whenever(
- appWidgetManager.bindAppWidgetIdIfAllowed(
- any<Int>(),
- any<UserHandle>(),
- any<ComponentName>(),
- any<Bundle>(),
- )
- )
- .thenReturn(true)
// bind the widget with the current user when no user is explicitly set
val result = underTest.allocateIdAndBindWidget(provider)
@@ -121,15 +133,6 @@
val user = UserHandle(0)
whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(widgetId)
- whenever(
- appWidgetManager.bindAppWidgetIdIfAllowed(
- any<Int>(),
- any<UserHandle>(),
- any<ComponentName>(),
- any<Bundle>()
- )
- )
- .thenReturn(true)
// provider and user handle are both set
val result = underTest.allocateIdAndBindWidget(provider, user)
@@ -172,6 +175,261 @@
assertThat(result).isNull()
}
+ @Test
+ fun listener_exactlyOneListenerRegisteredForEachWidgetWhenHostStartListening() =
+ testScope.runTest {
+ // 3 widgets registered with the host
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2, 3))
+
+ underTest.startObservingHost()
+ runCurrent()
+
+ // Make sure no listener is set before host starts listening
+ verify(appWidgetHost, never()).setListener(any(), any())
+
+ // Host starts listening
+ val observer =
+ withArgCaptor<CommunalAppWidgetHost.Observer> {
+ verify(appWidgetHost).addObserver(capture())
+ }
+ observer.onHostStartListening()
+ runCurrent()
+
+ // Verify a listener is set for each widget
+ verify(appWidgetHost, times(3)).setListener(any(), any())
+ verify(appWidgetHost).setListener(eq(1), any())
+ verify(appWidgetHost).setListener(eq(2), any())
+ verify(appWidgetHost).setListener(eq(3), any())
+ }
+
+ @Test
+ fun listener_listenersRemovedWhenHostStopListening() =
+ testScope.runTest {
+ // 3 widgets registered with the host
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2, 3))
+
+ underTest.startObservingHost()
+ runCurrent()
+
+ // Host starts listening
+ val observer =
+ withArgCaptor<CommunalAppWidgetHost.Observer> {
+ verify(appWidgetHost).addObserver(capture())
+ }
+ observer.onHostStartListening()
+ runCurrent()
+
+ // Verify none of the listener is removed before host stop listening
+ verify(appWidgetHost, never()).removeListener(any())
+
+ observer.onHostStopListening()
+
+ // Verify each listener is removed
+ verify(appWidgetHost, times(3)).removeListener(any())
+ verify(appWidgetHost).removeListener(eq(1))
+ verify(appWidgetHost).removeListener(eq(2))
+ verify(appWidgetHost).removeListener(eq(3))
+ }
+
+ @Test
+ fun listener_addNewListenerWhenNewIdAllocated() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf())
+ val observer = start()
+
+ // Verify no listener is set before a new app widget id is allocated
+ verify(appWidgetHost, never()).setListener(any(), any())
+
+ // Allocate an app widget id
+ observer.onAllocateAppWidgetId(1)
+
+ // Verify new listener set for that app widget id
+ verify(appWidgetHost).setListener(eq(1), any())
+ }
+
+ @Test
+ fun listener_removeListenerWhenWidgetDeleted() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1))
+ val observer = start()
+
+ // Verify listener not removed before widget deleted
+ verify(appWidgetHost, never()).removeListener(eq(1))
+
+ // Widget deleted
+ observer.onDeleteAppWidgetId(1)
+
+ // Verify listener removed for that widget
+ verify(appWidgetHost).removeListener(eq(1))
+ }
+
+ @Test
+ fun providerInfo_populatesWhenStartListening() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+ whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+ whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+ val providerInfoValues by collectValues(underTest.appWidgetProviders)
+
+ // Assert that the map is empty before host starts listening
+ assertThat(providerInfoValues).hasSize(1)
+ assertThat(providerInfoValues[0]).isEmpty()
+
+ start()
+ runCurrent()
+
+ // Assert that the provider info map is populated after host started listening, and that
+ // all providers are emitted at once
+ assertThat(providerInfoValues).hasSize(2)
+ assertThat(providerInfoValues[1])
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ )
+ )
+ }
+
+ @Test
+ fun providerInfo_clearsWhenStopListening() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+ whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+ whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+ val observer = start()
+ runCurrent()
+
+ // Assert that the provider info map is populated
+ val providerInfo by collectLastValue(underTest.appWidgetProviders)
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ )
+ )
+
+ // Host stop listening
+ observer.onHostStopListening()
+
+ // Assert that the provider info map is cleared
+ assertThat(providerInfo).isEmpty()
+ }
+
+ @Test
+ fun providerInfo_onUpdate() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+ whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+ whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+ val providerInfo by collectLastValue(underTest.appWidgetProviders)
+
+ start()
+ runCurrent()
+
+ // Assert that the provider info map is populated
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ )
+ )
+
+ // Provider info for widget 1 updated
+ val listener =
+ withArgCaptor<AppWidgetHost.AppWidgetHostListener> {
+ verify(appWidgetHost).setListener(eq(1), capture())
+ }
+ listener.onUpdateProviderInfo(providerInfo3)
+ runCurrent()
+
+ // Assert that the update is reflected in the flow
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo3),
+ Pair(2, providerInfo2),
+ )
+ )
+ }
+
+ @Test
+ fun providerInfo_updateWhenANewWidgetIsBound() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+ whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+ whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+ val providerInfo by collectLastValue(underTest.appWidgetProviders)
+
+ start()
+ runCurrent()
+
+ // Assert that the provider info map is populated
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ )
+ )
+
+ // Bind a new widget
+ whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(3)
+ whenever(appWidgetManager.getAppWidgetInfo(3)).thenReturn(providerInfo3)
+ val newWidgetComponentName = ComponentName.unflattenFromString("pkg_new/cls_new")!!
+ underTest.allocateIdAndBindWidget(newWidgetComponentName)
+ runCurrent()
+
+ // Assert that the new provider is reflected in the flow
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ Pair(3, providerInfo3),
+ )
+ )
+ }
+
+ @Test
+ fun providerInfo_updateWhenWidgetRemoved() =
+ testScope.runTest {
+ whenever(appWidgetHost.appWidgetIds).thenReturn(intArrayOf(1, 2))
+ whenever(appWidgetManager.getAppWidgetInfo(1)).thenReturn(providerInfo1)
+ whenever(appWidgetManager.getAppWidgetInfo(2)).thenReturn(providerInfo2)
+
+ val providerInfo by collectLastValue(underTest.appWidgetProviders)
+
+ val observer = start()
+ runCurrent()
+
+ // Assert that the provider info map is populated
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(1, providerInfo1),
+ Pair(2, providerInfo2),
+ )
+ )
+
+ // Remove widget 1
+ observer.onDeleteAppWidgetId(1)
+ runCurrent()
+
+ // Assert that provider info for widget 1 is removed
+ assertThat(providerInfo)
+ .containsExactlyEntriesIn(
+ mapOf(
+ Pair(2, providerInfo2),
+ )
+ )
+ }
+
private fun selectUser() {
kosmos.fakeUserRepository.selectedUser.value =
SelectedUserModel(
@@ -179,4 +437,16 @@
selectionStatus = SelectionStatus.SELECTION_COMPLETE
)
}
+
+ private fun TestScope.start(): CommunalAppWidgetHost.Observer {
+ underTest.startObservingHost()
+ runCurrent()
+
+ val observer =
+ withArgCaptor<CommunalAppWidgetHost.Observer> {
+ verify(appWidgetHost).addObserver(capture())
+ }
+ observer.onHostStartListening()
+ return observer
+ }
}
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 2b3f40f..e3dd9ae 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -170,7 +170,7 @@
@Test
public void testBurnInProtectionStopsWhenContentViewDetached() {
mController.onViewDetached();
- verify(mHandler).removeCallbacks(any(Runnable.class));
+ verify(mHandler).removeCallbacksAndMessages(null);
}
@Test
@@ -281,4 +281,16 @@
verify(mAnimationsController).cancelAnimations();
}
+
+ @Test
+ public void onViewAttached_addsScrimExpansionCallback() {
+ mController.onViewAttached();
+ verify(mBouncerlessScrimController).addCallback(any());
+ }
+
+ @Test
+ public void onViewDetached_removesScrimExpansionCallback() {
+ mController.onViewDetached();
+ verify(mBouncerlessScrimController).removeCallback(any());
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
index 3b6f6a2..f31eb7f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/UdfpsKeyguardInteractorTest.kt
@@ -58,7 +58,7 @@
@ExperimentalCoroutinesApi
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class UdfpsKeyguardInteractorTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class UdfpsKeyguardInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
val kosmos = testKosmos()
val testScope = kosmos.testScope
val keyguardRepository = kosmos.fakeKeyguardRepository
@@ -88,7 +88,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
index f52c66e..cde703b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModelTest.kt
@@ -43,7 +43,7 @@
@ExperimentalCoroutinesApi
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class AodToLockscreenTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class AodToLockscreenTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
val kosmos = testKosmos()
val testScope = kosmos.testScope
val repository = kosmos.fakeKeyguardTransitionRepository
@@ -60,7 +60,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
index fee18dd..d632936 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
@@ -16,13 +16,15 @@
package com.android.systemui.keyguard.ui.viewmodel
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.BrokenWithSceneContainer
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -31,29 +33,25 @@
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.shade.data.repository.shadeRepository
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.sysuiStatusBarStateController
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.milliseconds
-import kotlinx.coroutines.flow.MutableStateFlow
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.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class BouncerToGoneFlowsTest : SysuiTestCase() {
- @Mock private lateinit var shadeInteractor: ShadeInteractor
-
- private val shadeExpansionStateFlow = MutableStateFlow(0.1f)
+@RunWith(ParameterizedAndroidJunit4::class)
+class BouncerToGoneFlowsTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
@@ -61,16 +59,31 @@
}
private val testScope = kosmos.testScope
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
- private val shadeRepository = kosmos.shadeRepository
private val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
private val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor
- private val underTest = kosmos.bouncerToGoneFlows
+
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+ private lateinit var underTest: BouncerToGoneFlows
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(false)
sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(false)
+ underTest = kosmos.bouncerToGoneFlows
}
@Test
@@ -79,7 +92,7 @@
val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
runCurrent()
- shadeRepository.setLockscreenShadeExpansion(1f)
+ shadeTestUtil.setLockscreenShadeExpansion(1f)
whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
keyguardTransitionRepository.sendTransitionSteps(
@@ -99,12 +112,13 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun scrimAlpha_runDimissFromKeyguard_shadeNotExpanded() =
testScope.runTest {
val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
runCurrent()
- shadeRepository.setLockscreenShadeExpansion(0f)
+ shadeTestUtil.setLockscreenShadeExpansion(0f)
whenever(primaryBouncerInteractor.willRunDismissFromKeyguard()).thenReturn(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 2e1765a..838b2a7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -60,7 +60,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardRootViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
@@ -75,7 +75,6 @@
private val viewState = ViewStateAccessor()
- // add to init block
companion object {
@JvmStatic
@Parameters(name = "{0}")
@@ -85,7 +84,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index ec2cb04..de4b999 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -47,7 +47,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class LockscreenContentViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos: Kosmos = testKosmos()
@@ -62,7 +62,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
index e3ae3ba..bc381f2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
@@ -49,7 +49,7 @@
@ExperimentalCoroutinesApi
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class LockscreenToAodTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class LockscreenToAodTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(FULL_SCREEN_USER_SWITCHER, false) }
@@ -73,7 +73,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index adeb395..9337793 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -51,7 +51,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class LockscreenToDreamingTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class LockscreenToDreamingTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
@@ -73,7 +73,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index f8da74f..6ce7e88 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -54,7 +54,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class LockscreenToOccludedTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
@@ -76,7 +76,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
index d5df159..58c6817 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
@@ -46,7 +46,7 @@
@ExperimentalCoroutinesApi
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterization?) :
+class LockscreenToPrimaryBouncerTransitionViewModelTest(flags: FlagsParameterization) :
SysuiTestCase() {
private val kosmos =
testKosmos().apply {
@@ -67,7 +67,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt
new file mode 100644
index 0000000..1e5599b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryTest.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.content.ComponentName
+import android.content.packageManager
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.content.pm.UserInfo
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.kosmos.mainCoroutineContext
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class IconAndNameCustomRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private val packageManager: PackageManager = kosmos.packageManager
+ private val userTracker: FakeUserTracker =
+ kosmos.fakeUserTracker.apply {
+ whenever(userContext.packageManager).thenReturn(packageManager)
+ }
+
+ private val service1 =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component1,
+ tileService1,
+ drawable1,
+ appName1,
+ )
+
+ private val service2 =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component2,
+ tileService2,
+ drawable2,
+ appName2,
+ )
+
+ private val underTest =
+ with(kosmos) {
+ IconAndNameCustomRepository(
+ installedTilesRepository,
+ userTracker,
+ mainCoroutineContext,
+ )
+ }
+
+ @Before
+ fun setUp() {
+ kosmos.fakeInstalledTilesRepository.setInstalledServicesForUser(
+ userTracker.userId,
+ listOf(service1, service2)
+ )
+ }
+
+ @Test
+ fun loadDataForCurrentServices() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTileDataList = underTest.getCustomTileData()
+ val expectedData1 =
+ EditTileData(
+ TileSpec.create(component1),
+ Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)),
+ Text.Loaded(tileService1),
+ Text.Loaded(appName1),
+ )
+ val expectedData2 =
+ EditTileData(
+ TileSpec.create(component2),
+ Icon.Loaded(drawable2, ContentDescription.Loaded(tileService2)),
+ Text.Loaded(tileService2),
+ Text.Loaded(appName2),
+ )
+
+ assertThat(editTileDataList).containsExactly(expectedData1, expectedData2)
+ }
+ }
+
+ @Test
+ fun loadDataForCurrentServices_otherCurrentUser_empty() =
+ with(kosmos) {
+ testScope.runTest {
+ userTracker.set(listOf(UserInfo(11, "", 0)), 0)
+ val editTileDataList = underTest.getCustomTileData()
+
+ assertThat(editTileDataList).isEmpty()
+ }
+ }
+
+ @Test
+ fun loadDataForCurrentServices_serviceInfoWithNullIcon_notInList() =
+ with(kosmos) {
+ testScope.runTest {
+ val serviceNullIcon =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component2,
+ tileService2,
+ )
+ fakeInstalledTilesRepository.setInstalledServicesForUser(
+ userTracker.userId,
+ listOf(service1, serviceNullIcon)
+ )
+
+ val expectedData1 =
+ EditTileData(
+ TileSpec.create(component1),
+ Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)),
+ Text.Loaded(tileService1),
+ Text.Loaded(appName1),
+ )
+
+ val editTileDataList = underTest.getCustomTileData()
+ assertThat(editTileDataList).containsExactly(expectedData1)
+ }
+ }
+
+ private companion object {
+ val drawable1 = TestStubDrawable("drawable1")
+ val appName1 = "App1"
+ val tileService1 = "Tile Service 1"
+ val component1 = ComponentName("pkg1", "srv1")
+
+ val drawable2 = TestStubDrawable("drawable2")
+ val appName2 = "App2"
+ val tileService2 = "Tile Service 2"
+ val component2 = ComponentName("pkg2", "srv2")
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt
new file mode 100644
index 0000000..56cead1
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryTest.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.panels.data.repository
+
+import android.content.res.mainResources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class StockTilesRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ private val underTest = StockTilesRepository(kosmos.mainResources)
+
+ @Test
+ fun stockTilesMatchesResources() {
+ val expected =
+ kosmos.mainResources
+ .getString(R.string.quick_settings_tiles_stock)
+ .split(",")
+ .map(TileSpec::create)
+ assertThat(underTest.stockTiles).isEqualTo(expected)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.kt
new file mode 100644
index 0000000..deefbf5
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorTest.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.qs.panels.domain.interactor
+
+import android.content.ComponentName
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.panels.data.repository.iconAndNameCustomRepository
+import com.android.systemui.qs.panels.data.repository.stockTilesRepository
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
+import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig
+import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.fakeQSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider
+import com.android.systemui.settings.userTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EditTilesListInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ // Only have some configurations so we can test the effect of missing configurations.
+ // As the configurations are injected by dagger, we'll have all the existing configurations
+ private val internetTileConfig = kosmos.qsInternetTileConfig
+ private val flashlightTileConfig = kosmos.qsFlashlightTileConfig
+ private val batteryTileConfig = kosmos.qsBatterySaverTileConfig
+
+ private val serviceInfo =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component,
+ tileName,
+ icon,
+ appName,
+ )
+
+ private val underTest =
+ with(kosmos) {
+ EditTilesListInteractor(
+ stockTilesRepository,
+ qSTileConfigProvider,
+ iconAndNameCustomRepository,
+ )
+ }
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ fakeInstalledTilesRepository.setInstalledServicesForUser(
+ userTracker.userId,
+ listOf(serviceInfo)
+ )
+
+ with(fakeQSTileConfigProvider) {
+ putConfig(internetTileConfig.tileSpec, internetTileConfig)
+ putConfig(flashlightTileConfig.tileSpec, flashlightTileConfig)
+ putConfig(batteryTileConfig.tileSpec, batteryTileConfig)
+ }
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_stockTilesHaveNoAppName() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTiles = underTest.getTilesToEdit()
+
+ assertThat(editTiles.stockTiles.all { it.appName == null }).isTrue()
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_stockTilesAreAllPlatformSpecs() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTiles = underTest.getTilesToEdit()
+
+ assertThat(editTiles.stockTiles.all { it.tileSpec is TileSpec.PlatformTileSpec })
+ .isTrue()
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_stockTiles_sameOrderAsRepository() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTiles = underTest.getTilesToEdit()
+
+ assertThat(editTiles.stockTiles.map { it.tileSpec })
+ .isEqualTo(stockTilesRepository.stockTiles)
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_customTileData_matchesService() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTiles = underTest.getTilesToEdit()
+ val expected =
+ EditTileData(
+ tileSpec = TileSpec.create(component),
+ icon = Icon.Loaded(icon, ContentDescription.Loaded(tileName)),
+ label = Text.Loaded(tileName),
+ appName = Text.Loaded(appName),
+ )
+
+ assertThat(editTiles.customTiles).hasSize(1)
+ assertThat(editTiles.customTiles[0]).isEqualTo(expected)
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_tilesInConfigProvider_correctData() =
+ with(kosmos) {
+ testScope.runTest {
+ val editTiles = underTest.getTilesToEdit()
+
+ assertThat(
+ editTiles.stockTiles.first { it.tileSpec == internetTileConfig.tileSpec }
+ )
+ .isEqualTo(internetTileConfig.toEditTileData())
+ assertThat(
+ editTiles.stockTiles.first { it.tileSpec == flashlightTileConfig.tileSpec }
+ )
+ .isEqualTo(flashlightTileConfig.toEditTileData())
+ assertThat(editTiles.stockTiles.first { it.tileSpec == batteryTileConfig.tileSpec })
+ .isEqualTo(batteryTileConfig.toEditTileData())
+ }
+ }
+
+ @Test
+ fun getTilesToEdit_tilesNotInConfigProvider_useDefaultData() =
+ with(kosmos) {
+ testScope.runTest {
+ underTest
+ .getTilesToEdit()
+ .stockTiles
+ .filterNot { qSTileConfigProvider.hasConfig(it.tileSpec.spec) }
+ .forEach { assertThat(it).isEqualTo(it.tileSpec.missingConfigEditTileData()) }
+ }
+ }
+
+ private companion object {
+ val component = ComponentName("pkg", "srv")
+ const val tileName = "Tile Service"
+ const val appName = "App"
+ val icon = TestStubDrawable("icon")
+
+ fun TileSpec.missingConfigEditTileData(): EditTileData {
+ return EditTileData(
+ tileSpec = this,
+ icon = Icon.Resource(android.R.drawable.star_on, ContentDescription.Loaded(spec)),
+ label = Text.Loaded(spec),
+ appName = null
+ )
+ }
+
+ fun QSTileConfig.toEditTileData(): EditTileData {
+ return EditTileData(
+ tileSpec = tileSpec,
+ icon =
+ Icon.Resource(uiConfig.iconRes, ContentDescription.Resource(uiConfig.labelRes)),
+ label = Text.Resource(uiConfig.labelRes),
+ appName = null,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt
new file mode 100644
index 0000000..9fb25a28
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelTest.kt
@@ -0,0 +1,507 @@
+/*
+ * 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.panels.ui.viewmodel
+
+import android.R
+import android.content.ComponentName
+import android.graphics.drawable.TestStubDrawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
+import com.android.systemui.qs.panels.data.repository.stockTilesRepository
+import com.android.systemui.qs.panels.domain.interactor.editTilesListInteractor
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutMap
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutTypeInteractor
+import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeMinimumTilesRepository
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.minimumTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.qsTileFactory
+import com.android.systemui.qs.tiles.impl.alarm.qsAlarmTileConfig
+import com.android.systemui.qs.tiles.impl.battery.qsBatterySaverTileConfig
+import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig
+import com.android.systemui.qs.tiles.impl.internet.qsInternetTileConfig
+import com.android.systemui.qs.tiles.impl.sensorprivacy.qsCameraSensorPrivacyToggleTileConfig
+import com.android.systemui.qs.tiles.impl.sensorprivacy.qsMicrophoneSensorPrivacyToggleTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.fakeQSTileConfigProvider
+import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider
+import com.android.systemui.settings.userTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EditModeViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ // Only have some configurations so we can test the effect of missing configurations.
+ // As the configurations are injected by dagger, we'll have all the existing configurations
+ private val configs =
+ with(kosmos) {
+ setOf(
+ qsInternetTileConfig,
+ qsFlashlightTileConfig,
+ qsBatterySaverTileConfig,
+ qsAlarmTileConfig,
+ qsCameraSensorPrivacyToggleTileConfig,
+ qsMicrophoneSensorPrivacyToggleTileConfig,
+ )
+ }
+
+ private val serviceInfo1 =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component1,
+ tileService1,
+ drawable1,
+ appName1,
+ )
+
+ private val serviceInfo2 =
+ FakeInstalledTilesComponentRepository.ServiceInfo(
+ component2,
+ tileService2,
+ drawable2,
+ appName2,
+ )
+
+ private val underTest: EditModeViewModel by lazy {
+ with(kosmos) {
+ EditModeViewModel(
+ editTilesListInteractor,
+ currentTilesInteractor,
+ minimumTilesInteractor,
+ infiniteGridLayout,
+ applicationCoroutineScope,
+ gridLayoutTypeInteractor,
+ gridLayoutMap,
+ )
+ }
+ }
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ fakeMinimumTilesRepository = MinimumTilesFixedRepository(minNumberOfTiles)
+
+ fakeInstalledTilesRepository.setInstalledServicesForUser(
+ userTracker.userId,
+ listOf(serviceInfo1, serviceInfo2)
+ )
+
+ with(fakeQSTileConfigProvider) { configs.forEach { putConfig(it.tileSpec, it) } }
+ qsTileFactory = FakeQSFactory { FakeQSTile(userTracker.userId, available = true) }
+ }
+ }
+
+ @Test
+ fun isEditing() =
+ with(kosmos) {
+ testScope.runTest {
+ val isEditing by collectLastValue(underTest.isEditing)
+
+ assertThat(isEditing).isFalse()
+
+ underTest.startEditing()
+ assertThat(isEditing).isTrue()
+
+ underTest.stopEditing()
+ assertThat(isEditing).isFalse()
+ }
+ }
+
+ @Test
+ fun editing_false_emptyFlowOfTiles() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ assertThat(tiles).isNull()
+ }
+ }
+
+ @Test
+ fun editing_true_notEmptyTileData() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ underTest.startEditing()
+
+ assertThat(tiles).isNotEmpty()
+ }
+ }
+
+ @Test
+ fun tilesData_hasAllStockTiles() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ underTest.startEditing()
+
+ assertThat(
+ tiles!!
+ .filter { it.tileSpec is TileSpec.PlatformTileSpec }
+ .map { it.tileSpec }
+ )
+ .containsExactlyElementsIn(stockTilesRepository.stockTiles)
+ }
+ }
+
+ @Test
+ fun tilesData_stockTiles_haveCorrectUiValues() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filter { it.tileSpec is TileSpec.PlatformTileSpec }
+ .forEach {
+ val data = getEditTileData(it.tileSpec)
+
+ assertThat(it.label).isEqualTo(data.label)
+ assertThat(it.icon).isEqualTo(data.icon)
+ assertThat(it.appName).isNull()
+ }
+ }
+ }
+
+ @Test
+ fun tilesData_hasAllCustomTiles() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ underTest.startEditing()
+
+ assertThat(
+ tiles!!
+ .filter { it.tileSpec is TileSpec.CustomTileSpec }
+ .map { it.tileSpec }
+ )
+ .containsExactly(TileSpec.create(component1), TileSpec.create(component2))
+ }
+ }
+
+ @Test
+ fun tilesData_customTiles_haveCorrectUiValues() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+
+ underTest.startEditing()
+
+ // service1
+ val model1 = tiles!!.first { it.tileSpec == TileSpec.create(component1) }
+ assertThat(model1.label).isEqualTo(Text.Loaded(tileService1))
+ assertThat(model1.appName).isEqualTo(Text.Loaded(appName1))
+ assertThat(model1.icon)
+ .isEqualTo(Icon.Loaded(drawable1, ContentDescription.Loaded(tileService1)))
+
+ // service2
+ val model2 = tiles!!.first { it.tileSpec == TileSpec.create(component2) }
+ assertThat(model2.label).isEqualTo(Text.Loaded(tileService2))
+ assertThat(model2.appName).isEqualTo(Text.Loaded(appName2))
+ assertThat(model2.icon)
+ .isEqualTo(Icon.Loaded(drawable2, ContentDescription.Loaded(tileService2)))
+ }
+ }
+
+ @Test
+ fun currentTiles_inCorrectOrder_markedAsCurrent() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ listOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ assertThat(tiles!!.filter { it.isCurrent }.map { it.tileSpec })
+ .containsExactlyElementsIn(currentTiles)
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun notCurrentTiles() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ listOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ val remainingTiles =
+ stockTilesRepository.stockTiles.filterNot { it in currentTiles } +
+ listOf(TileSpec.create(component1))
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ assertThat(tiles!!.filterNot { it.isCurrent }.map { it.tileSpec })
+ .containsExactlyElementsIn(remainingTiles)
+ }
+ }
+
+ @Test
+ fun currentTilesChange_trackingChange() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ val newTile = TileSpec.create("internet")
+ val position = 1
+ currentTilesInteractor.addTile(newTile, position)
+ currentTiles.add(position, newTile)
+
+ assertThat(tiles!!.filter { it.isCurrent }.map { it.tileSpec })
+ .containsExactlyElementsIn(currentTiles)
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun nonCurrentTiles_orderPreservedWhenCurrentTilesChange() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ val nonCurrentSpecs = tiles!!.filterNot { it.isCurrent }.map { it.tileSpec }
+ val newTile = TileSpec.create("internet")
+ currentTilesInteractor.addTile(newTile)
+
+ assertThat(tiles!!.filterNot { it.isCurrent }.map { it.tileSpec })
+ .containsExactlyElementsIn(nonCurrentSpecs - listOf(newTile))
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun nonCurrentTiles_haveOnlyAddAction() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filterNot { it.isCurrent }
+ .forEach {
+ assertThat(it.availableEditActions)
+ .containsExactly(AvailableEditActions.ADD)
+ }
+ }
+ }
+
+ @Test
+ fun currentTiles_moreThanMinimumTiles_haveRemoveAction() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+ assertThat(currentTiles.size).isGreaterThan(minNumberOfTiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filter { it.isCurrent }
+ .forEach {
+ assertThat(it.availableEditActions).contains(AvailableEditActions.REMOVE)
+ }
+ }
+ }
+
+ @Test
+ fun currentTiles_minimumTiles_dontHaveRemoveAction() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+ assertThat(currentTiles.size).isEqualTo(minNumberOfTiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filter { it.isCurrent }
+ .forEach {
+ assertThat(it.availableEditActions)
+ .doesNotContain(AvailableEditActions.REMOVE)
+ }
+ }
+ }
+
+ @Test
+ fun currentTiles_lessThanMinimumTiles_dontHaveRemoveAction() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+ assertThat(currentTiles.size).isLessThan(minNumberOfTiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filter { it.isCurrent }
+ .forEach {
+ assertThat(it.availableEditActions)
+ .doesNotContain(AvailableEditActions.REMOVE)
+ }
+ }
+ }
+
+ @Test
+ fun currentTiles_haveMoveAction() =
+ with(kosmos) {
+ testScope.runTest {
+ val tiles by collectLastValue(underTest.tiles)
+ val currentTiles =
+ mutableListOf(
+ TileSpec.create("flashlight"),
+ TileSpec.create("airplane"),
+ TileSpec.create(component2),
+ TileSpec.create("alarm"),
+ )
+ currentTilesInteractor.setTiles(currentTiles)
+
+ underTest.startEditing()
+
+ tiles!!
+ .filter { it.isCurrent }
+ .forEach {
+ assertThat(it.availableEditActions).contains(AvailableEditActions.MOVE)
+ }
+ }
+ }
+
+ private companion object {
+ val drawable1 = TestStubDrawable("drawable1")
+ val appName1 = "App1"
+ val tileService1 = "Tile Service 1"
+ val component1 = ComponentName("pkg1", "srv1")
+
+ val drawable2 = TestStubDrawable("drawable2")
+ val appName2 = "App2"
+ val tileService2 = "Tile Service 2"
+ val component2 = ComponentName("pkg2", "srv2")
+
+ fun TileSpec.missingConfigEditTileData(): EditTileData {
+ return EditTileData(
+ tileSpec = this,
+ icon = Icon.Resource(R.drawable.star_on, ContentDescription.Loaded(spec)),
+ label = Text.Loaded(spec),
+ appName = null
+ )
+ }
+
+ fun QSTileConfig.toEditTileData(): EditTileData {
+ return EditTileData(
+ tileSpec = tileSpec,
+ icon =
+ Icon.Resource(uiConfig.iconRes, ContentDescription.Resource(uiConfig.labelRes)),
+ label = Text.Resource(uiConfig.labelRes),
+ appName = null,
+ )
+ }
+
+ fun Kosmos.getEditTileData(tileSpec: TileSpec): EditTileData {
+ return if (qSTileConfigProvider.hasConfig(tileSpec.spec)) {
+ qSTileConfigProvider.getConfig(tileSpec.spec).toEditTileData()
+ } else {
+ tileSpec.missingConfigEditTileData()
+ }
+ }
+
+ val minNumberOfTiles = 3
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
index bc57ce6..a0dec8c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
@@ -90,7 +90,7 @@
}
@Test
- fun componentsLoadedOnStart() =
+ fun servicesLoadedOnStart() =
testScope.runTest {
val userId = 0
val resolveInfo =
@@ -106,12 +106,14 @@
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
runCurrent()
+ val services = underTest.getInstalledTilesServiceInfos(userId)
assertThat(componentNames).containsExactly(TEST_COMPONENT)
+ assertThat(services).containsExactly(resolveInfo.serviceInfo)
}
@Test
- fun componentAdded_foundAfterPackageChange() =
+ fun serviceAdded_foundAfterPackageChange() =
testScope.runTest {
val userId = 0
val resolveInfo =
@@ -132,12 +134,14 @@
.thenReturn(listOf(resolveInfo))
kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty)
runCurrent()
+ val services = underTest.getInstalledTilesServiceInfos(userId)
assertThat(componentNames).containsExactly(TEST_COMPONENT)
+ assertThat(services).containsExactly(resolveInfo.serviceInfo)
}
@Test
- fun componentWithoutPermission_notValid() =
+ fun serviceWithoutPermission_notValid() =
testScope.runTest {
val userId = 0
val resolveInfo =
@@ -152,13 +156,15 @@
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ val services = underTest.getInstalledTilesServiceInfos(userId)
runCurrent()
assertThat(componentNames).isEmpty()
+ assertThat(services).isEmpty()
}
@Test
- fun componentNotEnabled_notValid() =
+ fun serviceNotEnabled_notValid() =
testScope.runTest {
val userId = 0
val resolveInfo =
@@ -173,9 +179,11 @@
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ val services = underTest.getInstalledTilesServiceInfos(userId)
runCurrent()
assertThat(componentNames).isEmpty()
+ assertThat(services).isEmpty()
}
@Test
@@ -221,30 +229,22 @@
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
runCurrent()
+ val service = underTest.getInstalledTilesServiceInfos(userId)
assertThat(componentNames).containsExactly(TEST_COMPONENT)
+ assertThat(service).containsExactly(resolveInfo.serviceInfo)
}
@Test
- fun loadComponentsForSameUserTwice_returnsSameFlow() =
+ fun loadServicesForSameUserTwice_returnsSameFlow() =
testScope.runTest {
- val flowForUser1 = underTest.getInstalledTilesComponents(1)
- val flowForUser1TheSecondTime = underTest.getInstalledTilesComponents(1)
+ val flowForUser1 = underTest.getInstalledTilesServiceInfos(1)
+ val flowForUser1TheSecondTime = underTest.getInstalledTilesServiceInfos(1)
runCurrent()
assertThat(flowForUser1TheSecondTime).isEqualTo(flowForUser1)
}
- @Test
- fun loadComponentsForDifferentUsers_returnsDifferentFlow() =
- testScope.runTest {
- val flowForUser1 = underTest.getInstalledTilesComponents(1)
- val flowForUser2 = underTest.getInstalledTilesComponents(2)
- runCurrent()
-
- assertThat(flowForUser2).isNotEqualTo(flowForUser1)
- }
-
// Tests that a ServiceInfo that is returned by queryIntentServicesAsUser but shortly
// after uninstalled, doesn't crash SystemUI.
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt
index 61e4774..3faab50 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AccessibilityTilesInteractorTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.data.repository.FakeAccessibilityQsShortcutsRepository
import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.domain.model.TileModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.ColorCorrectionTile
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
index 8ae9172..167eff1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/AutoAddInteractorTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dump.DumpManager
+import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.data.repository.FakeAutoAddRepository
import com.android.systemui.qs.pipeline.domain.autoaddable.FakeAutoAddable
import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 634c5fa..1c73fe2b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -32,6 +32,7 @@
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTile.BooleanState
import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.external.CustomTile
import com.android.systemui.qs.external.CustomTileStatePersister
import com.android.systemui.qs.external.TileLifecycleManager
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
index 90c8304..260189d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/NoLowNumberOfTilesTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.data.repository.FakeDefaultTilesRepository
import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
index 4207a9c..dffd0d7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.FakeQSFactory
+import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.data.repository.fakeRestoreRepository
import com.android.systemui.qs.pipeline.data.repository.fakeTileSpecRepository
@@ -54,9 +55,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
class WorkProfileAutoAddedAfterRestoreTest : SysuiTestCase() {
- private val kosmos by lazy {
- Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) }
- }
+ private val kosmos by lazy { Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) } }
// Getter here so it can change when there is a managed profile.
private val workTileAvailable: Boolean
get() = hasManagedProfile()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt
new file mode 100644
index 0000000..a0aa2d4
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractorTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.night.domain.interactor
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.util.time.DateFormatUtil
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import java.time.LocalTime
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileDataInteractorTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val testUser = UserHandle.of(1)!!
+ private val testStartTime = LocalTime.MIDNIGHT
+ private val testEndTime = LocalTime.NOON
+ private val colorDisplayManager =
+ mock<ColorDisplayManager> {
+ whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+ whenever(isNightDisplayActivated).thenReturn(false)
+ whenever(nightDisplayCustomStartTime).thenReturn(testStartTime)
+ whenever(nightDisplayCustomEndTime).thenReturn(testEndTime)
+ }
+ private val locationController = FakeLocationController(LeakCheck())
+ private val nightDisplayListener = mock<NightDisplayListener>()
+ private val listenerBuilder =
+ mock<NightDisplayListenerModule.Builder> {
+ whenever(setUser(anyInt())).thenReturn(this)
+ whenever(build()).thenReturn(nightDisplayListener)
+ }
+ private val globalSettings = kosmos.fakeGlobalSettings
+ private val secureSettings = kosmos.fakeSettings
+ private val dateFormatUtil = mock<DateFormatUtil> { whenever(is24HourFormat).thenReturn(false) }
+ private val testDispatcher = StandardTestDispatcher()
+ private val scope = TestScope(testDispatcher)
+ private val userScopedColorDisplayManager =
+ mock<UserScopedService<ColorDisplayManager>> {
+ whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+ }
+ private val nightDisplayRepository =
+ NightDisplayRepository(
+ testDispatcher,
+ scope.backgroundScope,
+ globalSettings,
+ secureSettings,
+ listenerBuilder,
+ userScopedColorDisplayManager,
+ locationController,
+ )
+
+ private val underTest: NightDisplayTileDataInteractor =
+ NightDisplayTileDataInteractor(context, dateFormatUtil, nightDisplayRepository)
+
+ @Test
+ fun availability_matchesColorDisplayManager() = runTest {
+ val availability by collectLastValue(underTest.availability(testUser))
+
+ val expectedAvailability = ColorDisplayManager.isNightDisplayAvailable(context)
+ assertThat(availability).isEqualTo(expectedAvailability)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..adc8bcb
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractorTest.kt
@@ -0,0 +1,177 @@
+/*
+ * 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.night.domain.interactor
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+import android.testing.LeakCheck
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.actions.intentInputs
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
+import com.android.systemui.qs.tiles.impl.custom.qsTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.fakeGlobalSettings
+import com.android.systemui.util.settings.fakeSettings
+import com.android.systemui.utils.leaks.FakeLocationController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileUserActionInteractorTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val qsTileIntentUserActionHandler = FakeQSTileIntentUserInputHandler()
+ private val testUser = UserHandle.of(1)
+ private val colorDisplayManager =
+ mock<ColorDisplayManager> {
+ whenever(nightDisplayAutoMode).thenReturn(ColorDisplayManager.AUTO_MODE_DISABLED)
+ whenever(isNightDisplayActivated).thenReturn(false)
+ }
+ private val locationController = FakeLocationController(LeakCheck())
+ private val nightDisplayListener = mock<NightDisplayListener>()
+ private val listenerBuilder =
+ mock<NightDisplayListenerModule.Builder> {
+ whenever(setUser(ArgumentMatchers.anyInt())).thenReturn(this)
+ whenever(build()).thenReturn(nightDisplayListener)
+ }
+ private val globalSettings = kosmos.fakeGlobalSettings
+ private val secureSettings = kosmos.fakeSettings
+ private val testDispatcher = StandardTestDispatcher()
+ private val scope = TestScope(testDispatcher)
+ private val userScopedColorDisplayManager =
+ mock<UserScopedService<ColorDisplayManager>> {
+ whenever(forUser(eq(testUser))).thenReturn(colorDisplayManager)
+ }
+ private val nightDisplayRepository =
+ NightDisplayRepository(
+ testDispatcher,
+ scope.backgroundScope,
+ globalSettings,
+ secureSettings,
+ listenerBuilder,
+ userScopedColorDisplayManager,
+ locationController,
+ )
+
+ private val underTest =
+ NightDisplayTileUserActionInteractor(
+ nightDisplayRepository,
+ qsTileIntentUserActionHandler,
+ kosmos.qsTileLogger
+ )
+
+ @Test
+ fun handleClick_inactive_activates() =
+ scope.runTest {
+ val startingModel = NightDisplayTileModel.AutoModeOff(false, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(true)
+ }
+
+ @Test
+ fun handleClick_active_disables() =
+ scope.runTest {
+ val startingModel = NightDisplayTileModel.AutoModeOff(true, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(false)
+ }
+
+ @Test
+ fun handleClick_whenAutoModeTwilight_flipsState() =
+ scope.runTest {
+ val originalState = true
+ val startingModel = NightDisplayTileModel.AutoModeTwilight(originalState, false, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(!originalState)
+ }
+
+ @Test
+ fun handleClick_whenAutoModeCustom_flipsState() =
+ scope.runTest {
+ val originalState = true
+ val startingModel =
+ NightDisplayTileModel.AutoModeCustom(originalState, false, null, null, false)
+
+ underTest.handleInput(QSTileInputTestKtx.click(startingModel, testUser))
+
+ verify(colorDisplayManager).setNightDisplayActivated(!originalState)
+ }
+
+ @Test
+ fun handleLongClickWhenEnabled() =
+ scope.runTest {
+ val enabledState = true
+
+ underTest.handleInput(
+ QSTileInputTestKtx.longClick(
+ NightDisplayTileModel.AutoModeOff(enabledState, false),
+ testUser
+ )
+ )
+
+ assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+
+ val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+ val actualIntentAction = intentInput.intent.action
+ val expectedIntentAction = Settings.ACTION_NIGHT_DISPLAY_SETTINGS
+ assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+ }
+
+ @Test
+ fun handleLongClickWhenDisabled() =
+ scope.runTest {
+ val enabledState = false
+
+ underTest.handleInput(
+ QSTileInputTestKtx.longClick(
+ NightDisplayTileModel.AutoModeOff(enabledState, false),
+ testUser
+ )
+ )
+
+ assertThat(qsTileIntentUserActionHandler.handledInputs).hasSize(1)
+
+ val intentInput = qsTileIntentUserActionHandler.intentInputs.last()
+ val actualIntentAction = intentInput.intent.action
+ val expectedIntentAction = Settings.ACTION_NIGHT_DISPLAY_SETTINGS
+ assertThat(actualIntentAction).isEqualTo(expectedIntentAction)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
new file mode 100644
index 0000000..5d2e701
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt
@@ -0,0 +1,315 @@
+/*
+ * 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.night.ui
+
+import android.graphics.drawable.TestStubDrawable
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+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.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.impl.night.qsNightDisplayTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import com.android.systemui.util.mockito.mock
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NightDisplayTileMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val config = kosmos.qsNightDisplayTileConfig
+
+ private val testStartTime = LocalTime.MIDNIGHT
+ private val testEndTime = LocalTime.NOON
+
+ private lateinit var mapper: NightDisplayTileMapper
+
+ @Before
+ fun setup() {
+ mapper =
+ NightDisplayTileMapper(
+ context.orCreateTestableResources
+ .apply {
+ addOverride(R.drawable.qs_nightlight_icon_on, TestStubDrawable())
+ addOverride(R.drawable.qs_nightlight_icon_off, TestStubDrawable())
+ }
+ .resources,
+ context.theme,
+ mock<QSTileLogger>(),
+ )
+ }
+
+ @Test
+ fun disabledModel_whenAutoModeOff() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(false, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_INACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ /** Force enable does not change the mode by itself. */
+ @Test
+ fun disabledModel_whenAutoModeOff_whenForceEnable() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(false, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_INACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_whenAutoModeOff() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(true, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_ACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_forceAutoMode_whenAutoModeOff() {
+ val inputModel = NightDisplayTileModel.AutoModeOff(true, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.resources.getStringArray(R.array.tile_states_night)[Tile.STATE_ACTIVE]
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeTwilight_locationOff() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(true, false, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState = createNightDisplayTileState(QSTileState.ActivationState.ACTIVE, null)
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeTwilight_locationOn() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(true, false, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(R.string.quick_settings_night_secondary_label_until_sunrise)
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeTwilight_locationOn() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(false, false, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(R.string.quick_settings_night_secondary_label_on_at_sunset)
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeTwilight_locationOff() {
+ val inputModel = NightDisplayTileModel.AutoModeTwilight(false, false, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState = createNightDisplayTileState(QSTileState.ActivationState.INACTIVE, null)
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeCustom_24Hour() {
+ val inputModel =
+ NightDisplayTileModel.AutoModeCustom(false, false, testStartTime, null, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(
+ R.string.quick_settings_night_secondary_label_on_at,
+ formatter24Hour.format(testStartTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_autoModeCustom_12Hour() {
+ val inputModel =
+ NightDisplayTileModel.AutoModeCustom(false, false, testStartTime, null, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(
+ R.string.quick_settings_night_secondary_label_on_at,
+ formatter12Hour.format(testStartTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ /** Should have the same outcome as [disabledModel_autoModeCustom_12Hour] */
+ @Test
+ fun disabledModel_autoModeCustom_12Hour_isEnrolledForcedAutoMode() {
+ val inputModel =
+ NightDisplayTileModel.AutoModeCustom(false, true, testStartTime, null, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.INACTIVE,
+ context.getString(
+ R.string.quick_settings_night_secondary_label_on_at,
+ formatter12Hour.format(testStartTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeCustom_24Hour() {
+ val inputModel = NightDisplayTileModel.AutoModeCustom(true, false, null, testEndTime, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(
+ R.string.quick_settings_secondary_label_until,
+ formatter24Hour.format(testEndTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun enabledModel_autoModeCustom_12Hour() {
+ val inputModel = NightDisplayTileModel.AutoModeCustom(true, false, null, testEndTime, false)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(
+ R.string.quick_settings_secondary_label_until,
+ formatter12Hour.format(testEndTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ /** Should have the same state as [enabledModel_autoModeCustom_24Hour] */
+ @Test
+ fun enabledModel_autoModeCustom_24Hour_forceEnabled() {
+ val inputModel = NightDisplayTileModel.AutoModeCustom(true, true, null, testEndTime, true)
+
+ val outputState = mapper.map(config, inputModel)
+
+ val expectedState =
+ createNightDisplayTileState(
+ QSTileState.ActivationState.ACTIVE,
+ context.getString(
+ R.string.quick_settings_secondary_label_until,
+ formatter24Hour.format(testEndTime)
+ )
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ private fun createNightDisplayTileState(
+ activationState: QSTileState.ActivationState,
+ secondaryLabel: String?
+ ): QSTileState {
+ val label = context.getString(R.string.quick_settings_night_display_label)
+
+ val contentDescription =
+ if (TextUtils.isEmpty(secondaryLabel)) label
+ else TextUtils.concat(label, ", ", secondaryLabel)
+ return QSTileState(
+ {
+ Icon.Loaded(
+ context.getDrawable(
+ if (activationState == QSTileState.ActivationState.ACTIVE)
+ R.drawable.qs_nightlight_icon_on
+ else R.drawable.qs_nightlight_icon_off
+ )!!,
+ null
+ )
+ },
+ label,
+ activationState,
+ secondaryLabel,
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+ contentDescription,
+ null,
+ QSTileState.SideViewIcon.None,
+ QSTileState.EnabledState.ENABLED,
+ Switch::class.qualifiedName
+ )
+ }
+
+ private companion object {
+ val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a")
+ val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt
new file mode 100644
index 0000000..a2ffe70
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModelTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.scene.ui.viewmodel
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+@EnableSceneContainer
+class GoneSceneViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val shadeRepository by lazy { kosmos.shadeRepository }
+ private lateinit var underTest: GoneSceneViewModel
+
+ @Before
+ fun setUp() {
+ underTest =
+ GoneSceneViewModel(
+ applicationScope = testScope.backgroundScope,
+ shadeInteractor = kosmos.shadeInteractor,
+ )
+ }
+
+ @Test
+ fun downTransitionKey_splitShadeEnabled_isGoneToSplitShade() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ shadeRepository.setShadeMode(ShadeMode.Split)
+ runCurrent()
+
+ assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey)
+ .isEqualTo(GoneToSplitShade)
+ }
+
+ @Test
+ fun downTransitionKey_splitShadeDisabled_isNull() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ shadeRepository.setShadeMode(ShadeMode.Single)
+ runCurrent()
+
+ assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.transitionKey).isNull()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index aa0ca18..78c4def 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -60,7 +60,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class ShadeInteractorImplTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() {
val kosmos = testKosmos()
val testScope = kosmos.testScope
val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
@@ -85,7 +85,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
index 44c9695..cecc70c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/startable/ShadeStartableTest.kt
@@ -60,7 +60,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class ShadeStartableTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class ShadeStartableTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val shadeInteractor by lazy { kosmos.shadeInteractor }
@@ -80,7 +80,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 5312ad8..2439217 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -39,6 +39,7 @@
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.scene.shared.model.TransitionKeys.GoneToSplitShade
import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -159,6 +160,27 @@
}
@Test
+ fun upTransitionKey_splitShadeEnabled_isGoneToSplitShade() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ shadeRepository.setShadeMode(ShadeMode.Split)
+ runCurrent()
+
+ assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey)
+ .isEqualTo(GoneToSplitShade)
+ }
+
+ @Test
+ fun upTransitionKey_splitShadeDisabled_isNull() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+ shadeRepository.setShadeMode(ShadeMode.Single)
+ runCurrent()
+
+ assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey).isNull()
+ }
+
+ @Test
fun isClickable_deviceUnlocked_false() =
testScope.runTest {
val isClickable by collectLastValue(underTest.isClickable)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index d353a62..f06e04b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -33,7 +33,10 @@
import com.android.systemui.flags.parameterizeSceneContainerFlag
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -65,11 +68,11 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
@TestableLooper.RunWithLooper
-class StatusBarStateControllerImplTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
-
+ private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
private val mockDarkAnimator = mock<ObjectAnimator>()
private lateinit var underTest: StatusBarStateControllerImpl
@@ -84,7 +87,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
@@ -98,6 +101,7 @@
uiEventLogger,
{ kosmos.interactionJankMonitor },
JavaAdapter(testScope.backgroundScope),
+ { kosmos.keyguardTransitionInteractor },
{ kosmos.shadeInteractor },
{ kosmos.deviceUnlockedInteractor },
{ kosmos.sceneInteractor },
@@ -330,4 +334,25 @@
assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
assertThat(statusBarState).isEqualTo(StatusBarState.SHADE)
}
+
+ @Test
+ fun leaveOpenOnKeyguard_whenGone_isFalse() =
+ testScope.runTest {
+ underTest.start()
+ underTest.setLeaveOpenOnKeyguardHide(true)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.AOD,
+ to = KeyguardState.LOCKSCREEN,
+ testScope = testScope,
+ )
+ assertThat(underTest.leaveOpenOnKeyguardHide()).isEqualTo(true)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope = testScope,
+ )
+ assertThat(underTest.leaveOpenOnKeyguardHide()).isEqualTo(false)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
index cbbc4d8..9367a93 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
@@ -59,7 +59,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class NotificationIconContainerStatusBarViewModelTest(flags: FlagsParameterization?) :
+class NotificationIconContainerStatusBarViewModelTest(flags: FlagsParameterization) :
SysuiTestCase() {
companion object {
@@ -71,7 +71,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 7ac549a..cc5df74 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -20,12 +20,13 @@
import android.app.NotificationManager.Policy
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.provider.Settings
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
@@ -33,7 +34,7 @@
import com.android.systemui.power.data.repository.fakePowerRepository
import com.android.systemui.power.shared.model.WakefulnessState
import com.android.systemui.res.R
-import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
@@ -56,11 +57,13 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@EnableFlags(FooterViewRefactor.FLAG_NAME)
-class NotificationListViewModelTest : SysuiTestCase() {
+class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
@@ -72,16 +75,30 @@
private val fakeKeyguardRepository = kosmos.fakeKeyguardRepository
private val fakePowerRepository = kosmos.fakePowerRepository
private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
- private val fakeShadeRepository = kosmos.fakeShadeRepository
private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
private val headsUpRepository = kosmos.headsUpNotificationRepository
private val zenModeRepository = kosmos.zenModeRepository
- val underTest = kosmos.notificationListViewModel
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+ private lateinit var underTest: NotificationListViewModel
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ underTest = kosmos.notificationListViewModel
}
@Test
@@ -163,7 +180,7 @@
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
// AND quick settings are expanded
- fakeShadeRepository.legacyQsFullscreen.value = true
+ shadeTestUtil.setQsFullscreen(true)
runCurrent()
// THEN empty shade is not visible
@@ -178,9 +195,10 @@
// WHEN has no notifs
activeNotificationListRepository.setActiveNotifs(count = 0)
// AND quick settings are expanded
- fakeShadeRepository.setQsExpansion(1f)
- // AND split shade is enabled
+ shadeTestUtil.setQsExpansion(1f)
+ // AND split shade is expanded
overrideResource(R.bool.config_use_split_notification_shade, true)
+ shadeTestUtil.setShadeExpansion(1f)
fakeConfigurationController.notifyConfigurationChanged()
runCurrent()
@@ -290,7 +308,7 @@
activeNotificationListRepository.setActiveNotifs(count = 2)
// AND shade is open
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
runCurrent()
// THEN footer is visible
@@ -306,7 +324,7 @@
activeNotificationListRepository.setActiveNotifs(count = 2)
// AND shade is open on lockscreen
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE_LOCKED)
- fakeShadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
runCurrent()
// THEN footer is visible
@@ -337,7 +355,7 @@
activeNotificationListRepository.setActiveNotifs(count = 2)
// AND shade is open
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
// AND user is not set up
fakeUserSetupRepository.setUserSetUp(false)
runCurrent()
@@ -355,7 +373,7 @@
activeNotificationListRepository.setActiveNotifs(count = 2)
// AND shade is open
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
// AND device is starting to go to sleep
fakePowerRepository.updateWakefulness(WakefulnessState.STARTING_TO_SLEEP)
runCurrent()
@@ -373,10 +391,10 @@
activeNotificationListRepository.setActiveNotifs(count = 2)
// AND shade is open
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
// AND quick settings are expanded
- fakeShadeRepository.setQsExpansion(1f)
- fakeShadeRepository.legacyQsFullscreen.value = true
+ shadeTestUtil.setQsExpansion(1f)
+ shadeTestUtil.setQsFullscreen(true)
runCurrent()
// THEN footer is not visible
@@ -390,11 +408,11 @@
// WHEN has notifs
activeNotificationListRepository.setActiveNotifs(count = 2)
+ // AND quick settings are expanded
+ shadeTestUtil.setQsExpansion(1f)
// AND shade is open
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setLegacyShadeExpansion(1f)
- // AND quick settings are expanded
- fakeShadeRepository.setQsExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
// AND split shade is enabled
overrideResource(R.bool.config_use_split_notification_shade, true)
fakeConfigurationController.notifyConfigurationChanged()
@@ -413,7 +431,7 @@
activeNotificationListRepository.setActiveNotifs(count = 2)
// AND shade is open
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
// AND remote input is active
fakeRemoteInputRepository.isRemoteInputActive.value = true
runCurrent()
@@ -431,7 +449,7 @@
activeNotificationListRepository.setActiveNotifs(count = 2)
// AND shade is open and fully expanded
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
runCurrent()
// THEN footer visibility animates
@@ -447,7 +465,7 @@
activeNotificationListRepository.setActiveNotifs(count = 2)
// AND we are on the keyguard
fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
- fakeShadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
runCurrent()
// THEN footer visibility does not animate
@@ -461,7 +479,7 @@
// WHEN shade is closed
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setLegacyShadeExpansion(0f)
+ shadeTestUtil.setShadeExpansion(0f)
runCurrent()
// THEN footer is hidden
@@ -475,7 +493,7 @@
// WHEN shade is open
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setShadeExpansion(1f)
runCurrent()
// THEN footer is hidden
@@ -489,8 +507,8 @@
// WHEN QS partially open
fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
- fakeShadeRepository.setQsExpansion(0.5f)
- fakeShadeRepository.setLegacyShadeExpansion(0.5f)
+ shadeTestUtil.setQsExpansion(0.5f)
+ shadeTestUtil.setShadeExpansion(0.5f)
runCurrent()
// THEN footer is hidden
@@ -588,7 +606,7 @@
testScope.runTest {
val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
- fakeShadeRepository.setQsExpansion(0.0f)
+ shadeTestUtil.setQsExpansion(0.0f)
fakeKeyguardRepository.setKeyguardShowing(false)
runCurrent()
@@ -601,7 +619,7 @@
testScope.runTest {
val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled)
- fakeShadeRepository.setQsExpansion(0.0f)
+ shadeTestUtil.setQsExpansion(0.0f)
fakeKeyguardRepository.setKeyguardShowing(true)
runCurrent()
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 2cd295c..f2ce745 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
@@ -75,7 +75,7 @@
@RunWith(ParameterizedAndroidJunit4::class)
// SharedNotificationContainerViewModel is only bound when FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT is on
@EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT)
-class SharedNotificationContainerViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
companion object {
@JvmStatic
@@ -89,7 +89,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
val aodBurnInViewModel = mock(AodBurnInViewModel::class.java)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
index 8ce5037..63f19fb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt
@@ -249,22 +249,40 @@
@Test
- fun testDelete_showingEntryKeyBecomesPreviousHunKey() {
+ fun testDelete_deleteSecondToLastEntry_showingEntryKeyBecomesPreviousHunKey() {
mAvalancheController.previousHunKey = ""
// Entry is showing
+ val firstEntry = createHeadsUpEntry(id = 0)
+ mAvalancheController.headsUpEntryShowing = firstEntry
+
+ // There's another entry waiting to show next
+ val secondEntry = createHeadsUpEntry(id = 1)
+ mAvalancheController.addToNext(secondEntry, runnableMock!!)
+
+ // Delete
+ mAvalancheController.delete(firstEntry, runnableMock, "testLabel")
+
+ // Next entry is shown
+ assertThat(mAvalancheController.previousHunKey).isEqualTo(firstEntry.mEntry!!.key)
+ }
+
+ @Test
+ fun testDelete_deleteLastEntry_previousHunKeyCleared() {
+ mAvalancheController.previousHunKey = "key"
+
+ // Nothing waiting to show
+ mAvalancheController.clearNext()
+
+ // One entry is showing
val showingEntry = createHeadsUpEntry(id = 0)
mAvalancheController.headsUpEntryShowing = showingEntry
- // There's another entry waiting to show next
- val nextEntry = createHeadsUpEntry(id = 1)
- mAvalancheController.addToNext(nextEntry, runnableMock!!)
-
// Delete
- mAvalancheController.delete(showingEntry, runnableMock, "testLabel")
+ mAvalancheController.delete(showingEntry, runnableMock!!, "testLabel")
// Next entry is shown
- assertThat(mAvalancheController.previousHunKey).isEqualTo(showingEntry.mEntry!!.key)
+ assertThat(mAvalancheController.previousHunKey).isEqualTo("");
}
@Test
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 45bcd82..b5ec5b2 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1158,6 +1158,8 @@
<string name="button_to_configure_widgets_text">Customize widgets</string>
<!-- Description for the App icon of disabled widget. [CHAR LIMIT=NONE] -->
<string name="icon_description_for_disabled_widget">App icon for disabled widget</string>
+ <!-- Description for the App icon of a package that is currently being installed. [CHAR LIMIT=NONE] -->
+ <string name="icon_description_for_pending_widget">App icon for a widget being installed</string>
<!-- Label for the button which configures widgets [CHAR LIMIT=NONE] -->
<string name="edit_widget">Edit widget</string>
<!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] -->
@@ -1634,9 +1636,15 @@
<string name="volume_panel_collapsed_sliders">Volume sliders collapsed</string>
<!-- Hint for accessibility. A stream name is a parameter. For example: double tap to mute media [CHAR_LIMIT=NONE] -->
- <string name="volume_panel_hint_mute">mute %s</string>
+ <string name="volume_panel_hint_mute">Mute %s</string>
<!-- Hint for accessibility. A stream name is a parameter. For example: double tap to unmute media [CHAR_LIMIT=NONE] -->
- <string name="volume_panel_hint_unmute">unmute %s</string>
+ <string name="volume_panel_hint_unmute">Unmute %s</string>
+
+ <!-- Hint for accessibility. This is announced when the stream is muted [CHAR_LIMIT=NONE] -->
+ <string name="volume_panel_hint_muted">muted</string>
+
+ <!-- Hint for accessibility. This is announced when ring mode is set to Vibrate. [CHAR_LIMIT=NONE] -->
+ <string name="volume_panel_hint_vibrate">vibrate</string>
<!-- Title with application label for media output settings when there is media playing. [CHAR LIMIT=20] -->
<string name="media_output_label_title">Playing <xliff:g id="label" example="Music Player">%s</xliff:g> on</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 460779c..f33acf2 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -43,6 +43,7 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.lifecycle.repeatWhenAttached
@@ -432,6 +433,7 @@
listenForDozeAmountTransition(this)
listenForAnyStateToAodTransition(this)
listenForAnyStateToLockscreenTransition(this)
+ listenForAnyStateToDozingTransition(this)
} else {
listenForDozeAmount(this)
}
@@ -578,6 +580,21 @@
}
}
+ /**
+ * When keyguard is displayed due to pulsing notifications when AOD is off,
+ * we should make sure clock is in dozing state instead of LS state
+ */
+ @VisibleForTesting
+ internal fun listenForAnyStateToDozingTransition(scope: CoroutineScope): Job {
+ return scope.launch {
+ keyguardTransitionInteractor
+ .transitionStepsToState(DOZING)
+ .filter { it.transitionState == TransitionState.FINISHED }
+ .collect { handleDoze(1f) }
+ }
+ }
+
+
@VisibleForTesting
internal fun listenForDozing(scope: CoroutineScope): Job {
return scope.launch {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt
new file mode 100644
index 0000000..8f071e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayChangeEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.data.model
+
+import java.time.LocalTime
+
+sealed interface NightDisplayChangeEvent {
+ data class OnAutoModeChanged(val autoMode: Int) : NightDisplayChangeEvent
+ data class OnActivatedChanged(val isActivated: Boolean) : NightDisplayChangeEvent
+ data class OnCustomStartTimeChanged(val startTime: LocalTime?) : NightDisplayChangeEvent
+ data class OnCustomEndTimeChanged(val endTime: LocalTime?) : NightDisplayChangeEvent
+ data class OnForceAutoModeChanged(val shouldForceAutoMode: Boolean) : NightDisplayChangeEvent
+ data class OnLocationEnabledChanged(val locationEnabled: Boolean) : NightDisplayChangeEvent
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt
new file mode 100644
index 0000000..196876e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/model/NightDisplayState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.model
+
+import java.time.LocalTime
+
+/** models the state of NightDisplayRepository */
+data class NightDisplayState(
+ val autoMode: Int = 0,
+ val isActivated: Boolean = true,
+ val startTime: LocalTime? = null,
+ val endTime: LocalTime? = null,
+ val shouldForceAutoMode: Boolean = false,
+ val locationEnabled: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt
index ae9f57f..6032f0b 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityRepository.kt
@@ -18,7 +18,7 @@
import android.view.accessibility.AccessibilityManager
import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import dagger.Module
import dagger.Provides
import kotlinx.coroutines.channels.awaitClose
@@ -29,6 +29,8 @@
interface AccessibilityRepository {
/** @see [AccessibilityManager.isTouchExplorationEnabled] */
val isTouchExplorationEnabled: Flow<Boolean>
+ /** @see [AccessibilityManager.isEnabled] */
+ val isEnabled: Flow<Boolean>
companion object {
operator fun invoke(a11yManager: AccessibilityManager): AccessibilityRepository =
@@ -47,6 +49,15 @@
awaitClose { manager.removeTouchExplorationStateChangeListener(listener) }
}
.distinctUntilChanged()
+
+ override val isEnabled: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val listener = AccessibilityManager.AccessibilityStateChangeListener(::trySend)
+ manager.addAccessibilityStateChangeListener(listener)
+ trySend(manager.isEnabled)
+ awaitClose { manager.removeAccessibilityStateChangeListener(listener) }
+ }
+ .distinctUntilChanged()
}
@Module
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt
new file mode 100644
index 0000000..bf44fab
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/NightDisplayRepository.kt
@@ -0,0 +1,196 @@
+/*
+ * 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.data.repository
+
+import android.hardware.display.ColorDisplayManager
+import android.hardware.display.NightDisplayListener
+import android.os.UserHandle
+import android.provider.Settings
+import com.android.systemui.accessibility.data.model.NightDisplayChangeEvent
+import com.android.systemui.accessibility.data.model.NightDisplayState
+import com.android.systemui.dagger.NightDisplayListenerModule
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.user.utils.UserScopedService
+import com.android.systemui.util.kotlin.isLocationEnabledFlow
+import com.android.systemui.util.settings.GlobalSettings
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import java.time.LocalTime
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.scan
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+class NightDisplayRepository
+@Inject
+constructor(
+ @Background private val bgCoroutineContext: CoroutineContext,
+ @Application private val scope: CoroutineScope,
+ private val globalSettings: GlobalSettings,
+ private val secureSettings: SecureSettings,
+ private val nightDisplayListenerBuilder: NightDisplayListenerModule.Builder,
+ private val colorDisplayManagerUserScopedService: UserScopedService<ColorDisplayManager>,
+ private val locationController: LocationController,
+) {
+ private val stateFlowUserMap = mutableMapOf<Int, Flow<NightDisplayState>>()
+
+ fun nightDisplayState(user: UserHandle): Flow<NightDisplayState> =
+ stateFlowUserMap.getOrPut(user.identifier) {
+ return merge(
+ colorDisplayManagerChangeEventFlow(user),
+ shouldForceAutoMode(user).map {
+ NightDisplayChangeEvent.OnForceAutoModeChanged(it)
+ },
+ locationController.isLocationEnabledFlow().map {
+ NightDisplayChangeEvent.OnLocationEnabledChanged(it)
+ }
+ )
+ .scan(initialState(user)) { state, event ->
+ when (event) {
+ is NightDisplayChangeEvent.OnActivatedChanged ->
+ state.copy(isActivated = event.isActivated)
+ is NightDisplayChangeEvent.OnAutoModeChanged ->
+ state.copy(autoMode = event.autoMode)
+ is NightDisplayChangeEvent.OnCustomStartTimeChanged ->
+ state.copy(startTime = event.startTime)
+ is NightDisplayChangeEvent.OnCustomEndTimeChanged ->
+ state.copy(endTime = event.endTime)
+ is NightDisplayChangeEvent.OnForceAutoModeChanged ->
+ state.copy(shouldForceAutoMode = event.shouldForceAutoMode)
+ is NightDisplayChangeEvent.OnLocationEnabledChanged ->
+ state.copy(locationEnabled = event.locationEnabled)
+ }
+ }
+ .conflate()
+ .onStart { emit(initialState(user)) }
+ .flowOn(bgCoroutineContext)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), NightDisplayState())
+ }
+
+ /** Track changes in night display enabled state and its auto mode */
+ private fun colorDisplayManagerChangeEventFlow(user: UserHandle) = callbackFlow {
+ val nightDisplayListener = nightDisplayListenerBuilder.setUser(user.identifier).build()
+ val nightDisplayCallback =
+ object : NightDisplayListener.Callback {
+ override fun onActivated(activated: Boolean) {
+ trySend(NightDisplayChangeEvent.OnActivatedChanged(activated))
+ }
+
+ override fun onAutoModeChanged(autoMode: Int) {
+ trySend(NightDisplayChangeEvent.OnAutoModeChanged(autoMode))
+ }
+
+ override fun onCustomStartTimeChanged(startTime: LocalTime?) {
+ trySend(NightDisplayChangeEvent.OnCustomStartTimeChanged(startTime))
+ }
+
+ override fun onCustomEndTimeChanged(endTime: LocalTime?) {
+ trySend(NightDisplayChangeEvent.OnCustomEndTimeChanged(endTime))
+ }
+ }
+ nightDisplayListener.setCallback(nightDisplayCallback)
+ awaitClose { nightDisplayListener.setCallback(null) }
+ }
+
+ /** @return true when the option to force auto mode is available and a value has not been set */
+ private fun shouldForceAutoMode(userHandle: UserHandle): Flow<Boolean> =
+ combine(isForceAutoModeAvailable, isDisplayAutoModeRawNotSet(userHandle)) {
+ isForceAutoModeAvailable,
+ isDisplayAutoModeRawNotSet,
+ ->
+ isForceAutoModeAvailable && isDisplayAutoModeRawNotSet
+ }
+
+ private val isForceAutoModeAvailable: Flow<Boolean> =
+ globalSettings
+ .observerFlow(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME)
+ .onStart { emit(Unit) }
+ .map {
+ globalSettings.getString(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME) ==
+ NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+ }
+ .distinctUntilChanged()
+
+ /** Inspired by [ColorDisplayService.getNightDisplayAutoModeRawInternal] */
+ private fun isDisplayAutoModeRawNotSet(userHandle: UserHandle): Flow<Boolean> =
+ if (userHandle.identifier == UserHandle.USER_NULL) {
+ flowOf(IS_AUTO_MODE_RAW_NOT_SET_DEFAULT)
+ } else {
+ secureSettings
+ .observerFlow(userHandle.identifier, DISPLAY_AUTO_MODE_RAW_SETTING_NAME)
+ .onStart { emit(Unit) }
+ .map {
+ secureSettings.getIntForUser(
+ DISPLAY_AUTO_MODE_RAW_SETTING_NAME,
+ userHandle.identifier
+ ) == NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET
+ }
+ }
+ .distinctUntilChanged()
+
+ suspend fun setNightDisplayAutoMode(autoMode: Int, user: UserHandle) {
+ withContext(bgCoroutineContext) {
+ colorDisplayManagerUserScopedService.forUser(user).nightDisplayAutoMode = autoMode
+ }
+ }
+
+ suspend fun setNightDisplayActivated(activated: Boolean, user: UserHandle) {
+ withContext(bgCoroutineContext) {
+ colorDisplayManagerUserScopedService.forUser(user).isNightDisplayActivated = activated
+ }
+ }
+
+ private fun initialState(user: UserHandle): NightDisplayState {
+ val colorDisplayManager = colorDisplayManagerUserScopedService.forUser(user)
+ return NightDisplayState(
+ colorDisplayManager.nightDisplayAutoMode,
+ colorDisplayManager.isNightDisplayActivated,
+ colorDisplayManager.nightDisplayCustomStartTime,
+ colorDisplayManager.nightDisplayCustomEndTime,
+ globalSettings.getString(IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME) ==
+ NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE &&
+ secureSettings.getIntForUser(DISPLAY_AUTO_MODE_RAW_SETTING_NAME, user.identifier) ==
+ NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET,
+ locationController.isLocationEnabled,
+ )
+ }
+
+ private companion object {
+ const val NIGHT_DISPLAY_AUTO_MODE_RAW_NOT_SET = -1
+ const val NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE = "1"
+ const val IS_AUTO_MODE_RAW_NOT_SET_DEFAULT = true
+ const val IS_FORCE_AUTO_MODE_AVAILABLE_SETTING_NAME =
+ Settings.Global.NIGHT_DISPLAY_FORCED_AUTO_MODE_AVAILABLE
+ const val DISPLAY_AUTO_MODE_RAW_SETTING_NAME = Settings.Secure.NIGHT_DISPLAY_AUTO_MODE
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt
index 968ce0d..93b624a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/domain/interactor/AccessibilityInteractor.kt
@@ -28,6 +28,8 @@
private val a11yRepo: AccessibilityRepository,
) {
/** @see [android.view.accessibility.AccessibilityManager.isTouchExplorationEnabled] */
- val isTouchExplorationEnabled: Flow<Boolean>
- get() = a11yRepo.isTouchExplorationEnabled
+ val isTouchExplorationEnabled: Flow<Boolean> = a11yRepo.isTouchExplorationEnabled
+
+ /** @see [android.view.accessibility.AccessibilityManager.isEnabled] */
+ val isEnabled: Flow<Boolean> = a11yRepo.isEnabled
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
index 54dd6d0..ed9597d 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
@@ -41,6 +41,10 @@
import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionTileDataInteractor
import com.android.systemui.qs.tiles.impl.inversion.domain.interactor.ColorInversionUserActionInteractor
import com.android.systemui.qs.tiles.impl.inversion.domain.model.ColorInversionTileModel
+import com.android.systemui.qs.tiles.impl.night.domain.interactor.NightDisplayTileDataInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.interactor.NightDisplayTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.impl.night.ui.NightDisplayTileMapper
import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileDataInteractor
import com.android.systemui.qs.tiles.impl.onehanded.domain.OneHandedModeTileUserActionInteractor
import com.android.systemui.qs.tiles.impl.onehanded.domain.model.OneHandedModeTileModel
@@ -117,6 +121,7 @@
const val FONT_SCALING_TILE_SPEC = "font_scaling"
const val REDUCE_BRIGHTNESS_TILE_SPEC = "reduce_brightness"
const val ONE_HANDED_TILE_SPEC = "onehanded"
+ const val NIGHT_DISPLAY_TILE_SPEC = "night"
@Provides
@IntoMap
@@ -279,5 +284,41 @@
mapper,
)
else StubQSTileViewModel
+
+ @Provides
+ @IntoMap
+ @StringKey(NIGHT_DISPLAY_TILE_SPEC)
+ fun provideNightDisplayTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+ QSTileConfig(
+ tileSpec = TileSpec.create(NIGHT_DISPLAY_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.qs_nightlight_icon_off,
+ labelRes = R.string.quick_settings_night_display_label,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ )
+
+ /**
+ * Inject NightDisplay Tile into tileViewModelMap in QSModule. The tile is hidden behind a
+ * flag.
+ */
+ @Provides
+ @IntoMap
+ @StringKey(NIGHT_DISPLAY_TILE_SPEC)
+ fun provideNightDisplayTileViewModel(
+ factory: QSTileViewModelFactory.Static<NightDisplayTileModel>,
+ mapper: NightDisplayTileMapper,
+ stateInteractor: NightDisplayTileDataInteractor,
+ userActionInteractor: NightDisplayTileUserActionInteractor
+ ): QSTileViewModel =
+ if (Flags.qsNewTilesFuture())
+ factory.create(
+ TileSpec.create(NIGHT_DISPLAY_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
+ else StubQSTileViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
index 5c64dc6..1c16429 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepository.kt
@@ -18,6 +18,7 @@
import android.os.UserHandle
import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageInstallSession
import kotlinx.coroutines.flow.Flow
interface PackageChangeRepository {
@@ -28,4 +29,7 @@
* [UserHandle.USER_ALL] may be used to listen to all users.
*/
fun packageChanged(user: UserHandle): Flow<PackageChangeModel>
+
+ /** Emits a list of all known install sessions associated with the primary user. */
+ val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>>
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
index 712a352..41b03f1 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageChangeRepositoryImpl.kt
@@ -18,6 +18,7 @@
import android.os.UserHandle
import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -27,6 +28,7 @@
class PackageChangeRepositoryImpl
@Inject
constructor(
+ packageInstallerMonitor: PackageInstallerMonitor,
private val monitorFactory: PackageUpdateMonitor.Factory,
) : PackageChangeRepository {
/**
@@ -37,4 +39,7 @@
override fun packageChanged(user: UserHandle): Flow<PackageChangeModel> =
monitor.packageChanged.filter { user == UserHandle.ALL || user == it.user }
+
+ override val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>> =
+ packageInstallerMonitor.installSessionsForPrimaryUser
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
new file mode 100644
index 0000000..46db346
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.common.data.repository
+
+import android.content.pm.PackageInstaller
+import android.os.Handler
+import com.android.internal.annotations.GuardedBy
+import com.android.systemui.common.shared.model.PackageInstallSession
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.dagger.PackageChangeRepoLog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.dropWhile
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+
+/** Monitors package install sessions for all users. */
+@SysUISingleton
+class PackageInstallerMonitor
+@Inject
+constructor(
+ @Background private val bgHandler: Handler,
+ @Background private val bgScope: CoroutineScope,
+ @PackageChangeRepoLog logBuffer: LogBuffer,
+ private val packageInstaller: PackageInstaller,
+) : PackageInstaller.SessionCallback() {
+
+ private val logger = Logger(logBuffer, TAG)
+
+ @GuardedBy("sessions") private val sessions = mutableMapOf<Int, PackageInstallSession>()
+
+ private val _installSessions =
+ MutableStateFlow<List<PackageInstallSession>>(emptyList()).apply {
+ subscriptionCount
+ .map { count -> count > 0 }
+ .distinctUntilChanged()
+ // Drop initial false value
+ .dropWhile { !it }
+ .onEach { isActive ->
+ if (isActive) {
+ synchronized(sessions) {
+ sessions.putAll(
+ packageInstaller.allSessions
+ .map { session -> session.toModel() }
+ .associateBy { it.sessionId }
+ )
+ updateInstallerSessionsFlow()
+ }
+ packageInstaller.registerSessionCallback(
+ this@PackageInstallerMonitor,
+ bgHandler
+ )
+ } else {
+ synchronized(sessions) {
+ sessions.clear()
+ updateInstallerSessionsFlow()
+ }
+ packageInstaller.unregisterSessionCallback(this@PackageInstallerMonitor)
+ }
+ }
+ .launchIn(bgScope)
+ }
+
+ val installSessionsForPrimaryUser: Flow<List<PackageInstallSession>> =
+ _installSessions.asStateFlow()
+
+ /** Called when a new installer session is created. */
+ override fun onCreated(sessionId: Int) {
+ logger.i({ "session created $int1" }) { int1 = sessionId }
+ updateSession(sessionId)
+ }
+
+ /** Called when new installer session has finished. */
+ override fun onFinished(sessionId: Int, success: Boolean) {
+ logger.i({ "session finished $int1" }) { int1 = sessionId }
+ synchronized(sessions) {
+ sessions.remove(sessionId)
+ updateInstallerSessionsFlow()
+ }
+ }
+
+ /**
+ * Badging details for the session changed. For example, the app icon or label has been updated.
+ */
+ override fun onBadgingChanged(sessionId: Int) {
+ logger.i({ "session badging changed $int1" }) { int1 = sessionId }
+ updateSession(sessionId)
+ }
+
+ /**
+ * A session is considered active when there is ongoing forward progress being made. For
+ * example, a package started downloading.
+ */
+ override fun onActiveChanged(sessionId: Int, active: Boolean) {
+ // Active status updates are not tracked for now
+ }
+
+ override fun onProgressChanged(sessionId: Int, progress: Float) {
+ // Progress updates are not tracked for now
+ }
+
+ private fun updateSession(sessionId: Int) {
+ val session = packageInstaller.getSessionInfo(sessionId)
+
+ synchronized(sessions) {
+ if (session == null) {
+ sessions.remove(sessionId)
+ } else {
+ sessions[sessionId] = session.toModel()
+ }
+ updateInstallerSessionsFlow()
+ }
+ }
+
+ @GuardedBy("sessions")
+ private fun updateInstallerSessionsFlow() {
+ _installSessions.value = sessions.values.toList()
+ }
+
+ companion object {
+ const val TAG = "PackageInstallerMonitor"
+
+ private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession {
+ return PackageInstallSession(
+ sessionId = this.sessionId,
+ packageName = this.appPackageName,
+ icon = this.getAppIcon(),
+ user = this.user,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt
new file mode 100644
index 0000000..7025229
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/PackageInstallSession.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.common.shared.model
+
+import android.graphics.Bitmap
+import android.os.UserHandle
+
+/** Represents a session of a package being installed on device. */
+data class PackageInstallSession(
+ val sessionId: Int,
+ val packageName: String,
+ val icon: Bitmap?,
+ val user: UserHandle,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index 1f54e70..fdb797d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -17,14 +17,13 @@
package com.android.systemui.communal.data.repository
import android.app.backup.BackupManager
-import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.os.UserHandle
-import androidx.annotation.WorkerThread
+import com.android.systemui.common.data.repository.PackageChangeRepository
+import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.communal.data.backup.CommunalBackupUtils
-import com.android.systemui.communal.data.db.CommunalItemRank
import com.android.systemui.communal.data.db.CommunalWidgetDao
-import com.android.systemui.communal.data.db.CommunalWidgetItem
import com.android.systemui.communal.nano.CommunalHubState
import com.android.systemui.communal.proto.toCommunalHubState
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
@@ -36,13 +35,15 @@
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
-import com.android.systemui.util.kotlin.getValue
-import java.util.Optional
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -88,7 +89,6 @@
class CommunalWidgetRepositoryImpl
@Inject
constructor(
- appWidgetManagerOptional: Optional<AppWidgetManager>,
private val appWidgetHost: CommunalAppWidgetHost,
@Background private val bgScope: CoroutineScope,
@Background private val bgDispatcher: CoroutineDispatcher,
@@ -97,6 +97,7 @@
@CommunalLog logBuffer: LogBuffer,
private val backupManager: BackupManager,
private val backupUtils: CommunalBackupUtils,
+ packageChangeRepository: PackageChangeRepository,
) : CommunalWidgetRepository {
companion object {
const val TAG = "CommunalWidgetRepository"
@@ -104,12 +105,39 @@
private val logger = Logger(logBuffer, TAG)
- private val appWidgetManager by appWidgetManagerOptional
+ /** Widget metadata from database + matching [AppWidgetProviderInfo] if any. */
+ private val widgetEntries: Flow<List<CommunalWidgetEntry>> =
+ combine(
+ communalWidgetDao.getWidgets(),
+ communalWidgetHost.appWidgetProviders,
+ ) { entries, providers ->
+ entries.mapNotNull { (rank, widget) ->
+ CommunalWidgetEntry(
+ appWidgetId = widget.widgetId,
+ componentName = widget.componentName,
+ priority = rank.rank,
+ providerInfo = providers[widget.widgetId]
+ )
+ }
+ }
+ @OptIn(ExperimentalCoroutinesApi::class)
override val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
- communalWidgetDao
- .getWidgets()
- .map { it.mapNotNull(::mapToContentModel) }
+ widgetEntries
+ .flatMapLatest { widgetEntries ->
+ // If and only if any widget is missing provider info, combine with the package
+ // installer sessions flow to check whether they are pending installation. This can
+ // happen after widgets are freshly restored from a backup. In most cases, provider
+ // info is available to all widgets, and is unnecessary to involve an API call to
+ // the package installer.
+ if (widgetEntries.any { it.providerInfo == null }) {
+ packageChangeRepository.packageInstallSessionsForPrimaryUser.map { sessions ->
+ widgetEntries.mapNotNull { entry -> mapToContentModel(entry, sessions) }
+ }
+ } else {
+ flowOf(widgetEntries.map(::mapToContentModel))
+ }
+ }
// As this reads from a database and triggers IPCs to AppWidgetManager,
// it should be executed in the background.
.flowOn(bgDispatcher)
@@ -245,6 +273,9 @@
}
appWidgetHost.deleteAppWidgetId(widgetId)
}
+
+ // Providers may have changed
+ communalWidgetHost.refreshProviders()
}
}
@@ -255,16 +286,57 @@
}
}
- @WorkerThread
- private fun mapToContentModel(
- entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>
- ): CommunalWidgetContentModel? {
- val (_, widgetId) = entry.value
- val providerInfo = appWidgetManager?.getAppWidgetInfo(widgetId) ?: return null
- return CommunalWidgetContentModel(
- appWidgetId = widgetId,
- providerInfo = providerInfo,
- priority = entry.key.rank,
+ /**
+ * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with the assumption that the
+ * [AppWidgetProviderInfo] of the entry is available.
+ */
+ private fun mapToContentModel(entry: CommunalWidgetEntry): CommunalWidgetContentModel {
+ return CommunalWidgetContentModel.Available(
+ appWidgetId = entry.appWidgetId,
+ providerInfo = entry.providerInfo!!,
+ priority = entry.priority,
)
}
+
+ /**
+ * Maps a [CommunalWidgetEntry] to a [CommunalWidgetContentModel] with a list of install
+ * sessions. If the [AppWidgetProviderInfo] of the entry is absent, and its package is in the
+ * install sessions, the entry is mapped to a pending widget.
+ */
+ private fun mapToContentModel(
+ entry: CommunalWidgetEntry,
+ installSessions: List<PackageInstallSession>,
+ ): CommunalWidgetContentModel? {
+ if (entry.providerInfo != null) {
+ return CommunalWidgetContentModel.Available(
+ appWidgetId = entry.appWidgetId,
+ providerInfo = entry.providerInfo!!,
+ priority = entry.priority,
+ )
+ }
+
+ val session =
+ installSessions.firstOrNull {
+ it.packageName ==
+ ComponentName.unflattenFromString(entry.componentName)?.packageName
+ }
+ return if (session != null) {
+ CommunalWidgetContentModel.Pending(
+ appWidgetId = entry.appWidgetId,
+ priority = entry.priority,
+ packageName = session.packageName,
+ icon = session.icon,
+ user = session.user,
+ )
+ } else {
+ null
+ }
+ }
+
+ private data class CommunalWidgetEntry(
+ val appWidgetId: Int,
+ val componentName: String,
+ val priority: Int,
+ var providerInfo: AppWidgetProviderInfo? = null,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 5091a99..6b4cf79 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -403,19 +403,30 @@
updateOnWorkProfileBroadcastReceived,
) { widgets, allowedCategories, _ ->
widgets.map { widget ->
- if (widget.providerInfo.widgetCategory and allowedCategories != 0) {
- // At least one category this widget specified is allowed, so show it
- WidgetContent.Widget(
- appWidgetId = widget.appWidgetId,
- providerInfo = widget.providerInfo,
- appWidgetHost = appWidgetHost,
- inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
- )
- } else {
- WidgetContent.DisabledWidget(
- appWidgetId = widget.appWidgetId,
- providerInfo = widget.providerInfo,
- )
+ when (widget) {
+ is CommunalWidgetContentModel.Available -> {
+ if (widget.providerInfo.widgetCategory and allowedCategories != 0) {
+ // At least one category this widget specified is allowed, so show it
+ WidgetContent.Widget(
+ appWidgetId = widget.appWidgetId,
+ providerInfo = widget.providerInfo,
+ appWidgetHost = appWidgetHost,
+ inQuietMode = isQuietModeEnabled(widget.providerInfo.profile)
+ )
+ } else {
+ WidgetContent.DisabledWidget(
+ appWidgetId = widget.appWidgetId,
+ providerInfo = widget.providerInfo,
+ )
+ }
+ }
+ is CommunalWidgetContentModel.Pending -> {
+ WidgetContent.PendingWidget(
+ appWidgetId = widget.appWidgetId,
+ packageName = widget.packageName,
+ icon = widget.icon,
+ )
+ }
}
}
}
@@ -430,7 +441,15 @@
} else {
// Get associated work profile for the currently selected user.
val workProfile = userTracker.userProfiles.find { it.isManagedProfile }
- list.filter { it.providerInfo.profile.identifier != workProfile?.id }
+ list.filter { model ->
+ val uid =
+ when (model) {
+ is CommunalWidgetContentModel.Available ->
+ model.providerInfo.profile.identifier
+ is CommunalWidgetContentModel.Pending -> model.user.identifier
+ }
+ uid != workProfile?.id
+ }
}
/** A flow of available smartspace targets. Currently only showing timers. */
@@ -513,7 +532,11 @@
): List<CommunalWidgetContentModel> {
val currentUserIds = userTracker.userProfiles.map { it.id }.toSet()
return list.filter { widget ->
- currentUserIds.contains(widget.providerInfo.profile?.identifier)
+ when (widget) {
+ is CommunalWidgetContentModel.Available ->
+ currentUserIds.contains(widget.providerInfo.profile?.identifier)
+ is CommunalWidgetContentModel.Pending -> true
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
index 7061227..122240d 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt
@@ -19,6 +19,7 @@
import android.appwidget.AppWidgetProviderInfo
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE
import android.content.pm.ApplicationInfo
+import android.graphics.Bitmap
import android.widget.RemoteViews
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
@@ -45,11 +46,10 @@
sealed interface WidgetContent : CommunalContentModel {
val appWidgetId: Int
- val providerInfo: AppWidgetProviderInfo
data class Widget(
override val appWidgetId: Int,
- override val providerInfo: AppWidgetProviderInfo,
+ val providerInfo: AppWidgetProviderInfo,
val appWidgetHost: CommunalAppWidgetHost,
val inQuietMode: Boolean,
) : WidgetContent {
@@ -66,7 +66,7 @@
data class DisabledWidget(
override val appWidgetId: Int,
- override val providerInfo: AppWidgetProviderInfo
+ val providerInfo: AppWidgetProviderInfo
) : WidgetContent {
override val key = KEY.disabledWidget(appWidgetId)
// Widget size is always half.
@@ -75,6 +75,16 @@
val appInfo: ApplicationInfo?
get() = providerInfo.providerInfo?.applicationInfo
}
+
+ data class PendingWidget(
+ override val appWidgetId: Int,
+ val packageName: String,
+ val icon: Bitmap? = null,
+ ) : WidgetContent {
+ override val key = KEY.pendingWidget(appWidgetId)
+ // Widget size is always half.
+ override val size = CommunalContentSize.HALF
+ }
}
/** A placeholder item representing a new widget being added */
@@ -127,6 +137,10 @@
return "disabled_widget_$id"
}
+ fun pendingWidget(id: Int): String {
+ return "pending_widget_$id"
+ }
+
fun widgetPlaceholder(): String {
return "widget_placeholder_${UUID.randomUUID()}"
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
index e141dc4..53aecc1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt
@@ -17,10 +17,27 @@
package com.android.systemui.communal.shared.model
import android.appwidget.AppWidgetProviderInfo
+import android.graphics.Bitmap
+import android.os.UserHandle
/** Encapsulates data for a communal widget. */
-data class CommunalWidgetContentModel(
- val appWidgetId: Int,
- val providerInfo: AppWidgetProviderInfo,
- val priority: Int,
-)
+sealed interface CommunalWidgetContentModel {
+ val appWidgetId: Int
+ val priority: Int
+
+ /** Widget is ready to display */
+ data class Available(
+ override val appWidgetId: Int,
+ val providerInfo: AppWidgetProviderInfo,
+ override val priority: Int,
+ ) : CommunalWidgetContentModel
+
+ /** Widget is pending installation */
+ data class Pending(
+ override val appWidgetId: Int,
+ override val priority: Int,
+ val packageName: String,
+ val icon: Bitmap?,
+ val user: UserHandle,
+ ) : CommunalWidgetContentModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 3f92223..f6122ad 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -104,7 +104,12 @@
): Boolean =
withContext(backgroundDispatcher) {
val widgets = communalInteractor.widgetContent.first()
- val excludeList = widgets.mapTo(ArrayList()) { it.providerInfo }
+ val excludeList =
+ widgets.filterIsInstance<CommunalContentModel.WidgetContent.Widget>().mapTo(
+ ArrayList()
+ ) {
+ it.providerInfo
+ }
getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let {
try {
activityLauncher.launch(it)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
index 337d873..9114aab 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTransitionViewModel.kt
@@ -33,6 +33,7 @@
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
/** View model for transitions related to the communal hub. */
@@ -49,6 +50,27 @@
communalInteractor: CommunalInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
) {
+ // Show UMO on glanceable hub immediately on transition into glanceable hub
+ private val showUmoFromOccludedToGlanceableHub: Flow<Boolean> =
+ keyguardTransitionInteractor
+ .transitionStepsFromState(KeyguardState.OCCLUDED)
+ .filter {
+ it.to == KeyguardState.GLANCEABLE_HUB &&
+ (it.transitionState == TransitionState.STARTED ||
+ it.transitionState == TransitionState.CANCELED)
+ }
+ .map { it.transitionState == TransitionState.STARTED }
+
+ private val showUmoFromGlanceableHubToOccluded: Flow<Boolean> =
+ keyguardTransitionInteractor
+ .transitionStepsFromState(KeyguardState.GLANCEABLE_HUB)
+ .filter {
+ it.to == KeyguardState.OCCLUDED &&
+ (it.transitionState == TransitionState.FINISHED ||
+ it.transitionState == TransitionState.CANCELED)
+ }
+ .map { it.transitionState != TransitionState.FINISHED }
+
/**
* Whether UMO location should be on communal. This flow is responsive to transitions so that a
* new value is emitted at the right step of a transition to/from communal hub that the location
@@ -60,6 +82,8 @@
glanceableHubToLockscreenTransitionViewModel.showUmo,
dreamToGlanceableHubTransitionViewModel.showUmo,
glanceableHubToDreamTransitionViewModel.showUmo,
+ showUmoFromOccludedToGlanceableHub,
+ showUmoFromGlanceableHubToOccluded,
)
.distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
index 5f1d89e..b7e8205 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt
@@ -24,6 +24,7 @@
import android.widget.RemoteViews
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
+import javax.annotation.concurrent.GuardedBy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -47,6 +48,8 @@
/** App widget ids that have been removed and no longer available. */
val appWidgetIdToRemove: SharedFlow<Int> = _appWidgetIdToRemove.asSharedFlow()
+ @GuardedBy("observers") private val observers = mutableSetOf<Observer>()
+
override fun onCreateView(
context: Context,
appWidgetId: Int,
@@ -77,6 +80,61 @@
}
}
+ override fun allocateAppWidgetId(): Int {
+ return super.allocateAppWidgetId().also { appWidgetId ->
+ backgroundScope.launch {
+ observers.forEach { observer -> observer.onAllocateAppWidgetId(appWidgetId) }
+ }
+ }
+ }
+
+ override fun deleteAppWidgetId(appWidgetId: Int) {
+ super.deleteAppWidgetId(appWidgetId)
+ backgroundScope.launch {
+ observers.forEach { observer -> observer.onDeleteAppWidgetId(appWidgetId) }
+ }
+ }
+
+ override fun startListening() {
+ super.startListening()
+ backgroundScope.launch { observers.forEach { observer -> observer.onHostStartListening() } }
+ }
+
+ override fun stopListening() {
+ super.stopListening()
+ backgroundScope.launch { observers.forEach { observer -> observer.onHostStopListening() } }
+ }
+
+ fun addObserver(observer: Observer) {
+ synchronized(observers) { observers.add(observer) }
+ }
+
+ fun removeObserver(observer: Observer) {
+ synchronized(observers) { observers.remove(observer) }
+ }
+
+ /**
+ * Allows another class to observe the [CommunalAppWidgetHost] and handle any logic there.
+ *
+ * This is mainly for testability as it is difficult to test a real instance of [AppWidgetHost]
+ * which communicates with framework services.
+ *
+ * Note: all the callbacks are launched from the background scope.
+ */
+ interface Observer {
+ /** Called immediately after the host has started listening for widget updates. */
+ fun onHostStartListening() {}
+
+ /** Called immediately after the host has stopped listening for widget updates. */
+ fun onHostStopListening() {}
+
+ /** Called immediately after a new app widget id has been allocated. */
+ fun onAllocateAppWidgetId(appWidgetId: Int) {}
+
+ /** Called immediately after an app widget id is to be deleted. */
+ fun onDeleteAppWidgetId(appWidgetId: Int) {}
+ }
+
companion object {
private const val TAG = "CommunalAppWidgetHost"
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
index 2ccab07..301da51 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -39,6 +39,7 @@
@Inject
constructor(
private val appWidgetHost: CommunalAppWidgetHost,
+ private val communalWidgetHost: CommunalWidgetHost,
private val communalInteractor: CommunalInteractor,
private val userTracker: UserTracker,
@Background private val bgScope: CoroutineScope,
@@ -70,9 +71,11 @@
// Always ensure this is called on the main/ui thread.
withContext(uiDispatcher) {
if (active) {
+ communalWidgetHost.startObservingHost()
appWidgetHost.startListening()
} else {
appWidgetHost.stopListening()
+ communalWidgetHost.stopObservingHost()
}
}
@@ -83,7 +86,15 @@
private fun validateWidgetsAndDeleteOrphaned(widgets: List<CommunalWidgetContentModel>) {
val currentUserIds = userTracker.userProfiles.map { it.id }.toSet()
widgets
- .filter { widget -> !currentUserIds.contains(widget.providerInfo.profile?.identifier) }
+ .filter { widget ->
+ val uid =
+ when (widget) {
+ is CommunalWidgetContentModel.Available ->
+ widget.providerInfo.profile?.identifier
+ is CommunalWidgetContentModel.Pending -> widget.user.identifier
+ }
+ !currentUserIds.contains(uid)
+ }
.onEach { widget -> communalInteractor.deleteWidget(id = widget.appWidgetId) }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt
index 93e2b37..42107c1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt
@@ -16,6 +16,7 @@
package com.android.systemui.communal.widgets
+import android.appwidget.AppWidgetHost.AppWidgetHostListener
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProviderInfo
import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL
@@ -23,6 +24,9 @@
import android.content.ComponentName
import android.os.Bundle
import android.os.UserHandle
+import android.widget.RemoteViews
+import androidx.annotation.WorkerThread
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
@@ -30,6 +34,11 @@
import com.android.systemui.util.kotlin.getOrNull
import java.util.Optional
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
/**
* Widget host that interacts with AppWidget service and host to bind and provide info for widgets
@@ -38,11 +47,12 @@
class CommunalWidgetHost
@Inject
constructor(
+ @Background private val bgScope: CoroutineScope,
private val appWidgetManager: Optional<AppWidgetManager>,
private val appWidgetHost: CommunalAppWidgetHost,
private val selectedUserInteractor: SelectedUserInteractor,
@CommunalLog logBuffer: LogBuffer,
-) {
+) : CommunalAppWidgetHost.Observer {
companion object {
private const val TAG = "CommunalWidgetHost"
@@ -60,6 +70,19 @@
private val logger = Logger(logBuffer, TAG)
+ private val _appWidgetProviders = MutableStateFlow(emptyMap<Int, AppWidgetProviderInfo?>())
+
+ /**
+ * A flow of mappings between an appWidgetId and its corresponding [AppWidgetProviderInfo].
+ * These [AppWidgetProviderInfo]s represent app widgets that are actively bound to the
+ * [CommunalAppWidgetHost].
+ *
+ * The [AppWidgetProviderInfo] may be null in the case that the widget is bound but its provider
+ * is unavailable. For example, its package is not installed.
+ */
+ val appWidgetProviders: StateFlow<Map<Int, AppWidgetProviderInfo?>> =
+ _appWidgetProviders.asStateFlow()
+
/**
* Allocate an app widget id and binds the widget with the provider and associated user.
*
@@ -77,6 +100,7 @@
)
) {
logger.d("Successfully bound the widget $provider")
+ onProviderInfoUpdated(id, getAppWidgetInfo(id))
return id
}
appWidgetHost.deleteAppWidgetId(id)
@@ -100,7 +124,83 @@
return false
}
+ @WorkerThread
fun getAppWidgetInfo(widgetId: Int): AppWidgetProviderInfo? {
return appWidgetManager.getOrNull()?.getAppWidgetInfo(widgetId)
}
+
+ fun startObservingHost() {
+ appWidgetHost.addObserver(this@CommunalWidgetHost)
+ }
+
+ fun stopObservingHost() {
+ appWidgetHost.removeObserver(this@CommunalWidgetHost)
+ }
+
+ fun refreshProviders() {
+ bgScope.launch {
+ val newProviders = mutableMapOf<Int, AppWidgetProviderInfo?>()
+ appWidgetHost.appWidgetIds.forEach { appWidgetId ->
+ // Listen for updates from each bound widget
+ addListener(appWidgetId)
+
+ // Fetch provider info of the widget
+ newProviders[appWidgetId] = getAppWidgetInfo(appWidgetId)
+ }
+
+ _appWidgetProviders.value = newProviders.toMap()
+ }
+ }
+
+ override fun onHostStartListening() {
+ refreshProviders()
+ }
+
+ override fun onHostStopListening() {
+ // Remove listeners
+ _appWidgetProviders.value.keys.forEach { appWidgetId ->
+ appWidgetHost.removeListener(appWidgetId)
+ }
+
+ // Clear providers
+ _appWidgetProviders.value = emptyMap()
+ }
+
+ override fun onAllocateAppWidgetId(appWidgetId: Int) {
+ addListener(appWidgetId)
+ }
+
+ override fun onDeleteAppWidgetId(appWidgetId: Int) {
+ appWidgetHost.removeListener(appWidgetId)
+ _appWidgetProviders.value =
+ _appWidgetProviders.value.toMutableMap().also { it.remove(appWidgetId) }
+ }
+
+ private fun addListener(appWidgetId: Int) {
+ appWidgetHost.setListener(
+ appWidgetId,
+ CommunalAppWidgetHostListener(appWidgetId, this::onProviderInfoUpdated),
+ )
+ }
+
+ private fun onProviderInfoUpdated(appWidgetId: Int, providerInfo: AppWidgetProviderInfo?) {
+ bgScope.launch {
+ _appWidgetProviders.value =
+ _appWidgetProviders.value.toMutableMap().also { it[appWidgetId] = providerInfo }
+ }
+ }
+
+ /** A [AppWidgetHostListener] for [appWidgetId]. */
+ private class CommunalAppWidgetHostListener(
+ private val appWidgetId: Int,
+ private val onUpdateProviderInfo: (Int, AppWidgetProviderInfo?) -> Unit,
+ ) : AppWidgetHostListener {
+ override fun onUpdateProviderInfo(providerInfo: AppWidgetProviderInfo?) {
+ onUpdateProviderInfo(appWidgetId, providerInfo)
+ }
+
+ override fun onViewDataChanged(viewId: Int) {}
+
+ override fun updateAppWidget(remoteViews: RemoteViews?) {}
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
index aa6516d..2000f96 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt
@@ -69,16 +69,18 @@
@SysUISingleton
@Provides
fun provideCommunalWidgetHost(
+ @Application applicationScope: CoroutineScope,
appWidgetManager: Optional<AppWidgetManager>,
appWidgetHost: CommunalAppWidgetHost,
selectedUserInteractor: SelectedUserInteractor,
@CommunalLog logBuffer: LogBuffer,
): CommunalWidgetHost {
return CommunalWidgetHost(
+ applicationScope,
appWidgetManager,
appWidgetHost,
selectedUserInteractor,
- logBuffer
+ logBuffer,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index ef3f10f..11e6f7a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -49,6 +49,7 @@
import android.content.om.OverlayManager;
import android.content.pm.IPackageManager;
import android.content.pm.LauncherApps;
+import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutManager;
import android.content.res.AssetManager;
@@ -224,6 +225,13 @@
@Provides
@Singleton
+ static UserScopedService<ColorDisplayManager> provideScopedColorDisplayManager(
+ Context context) {
+ return new UserScopedServiceImpl<>(context, ColorDisplayManager.class);
+ }
+
+ @Provides
+ @Singleton
static CrossWindowBlurListeners provideCrossWindowBlurListeners() {
return CrossWindowBlurListeners.getInstance();
}
@@ -483,6 +491,12 @@
@Provides
@Singleton
+ static PackageInstaller providePackageInstaller(PackageManager packageManager) {
+ return packageManager.getPackageInstaller();
+ }
+
+ @Provides
+ @Singleton
static PackageManagerWrapper providePackageManagerWrapper() {
return PackageManagerWrapper.getInstance();
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 8c0a73c..6e04339 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -198,7 +198,6 @@
mLowLightTransitionCoordinator = lowLightTransitionCoordinator;
mBouncerlessScrimController = bouncerlessScrimController;
- mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback);
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
@@ -234,6 +233,7 @@
mJitterStartTimeMillis = System.currentTimeMillis();
mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval);
mPrimaryBouncerCallbackInteractor.addBouncerExpansionCallback(mBouncerExpansionCallback);
+ mBouncerlessScrimController.addCallback(mBouncerlessExpansionCallback);
final Region emptyRegion = Region.obtain();
mView.getRootSurfaceControl().setTouchableRegion(emptyRegion);
emptyRegion.recycle();
@@ -255,8 +255,9 @@
@Override
protected void onViewDetached() {
- mHandler.removeCallbacks(this::updateBurnInOffsets);
+ mHandler.removeCallbacksAndMessages(null);
mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
+ mBouncerlessScrimController.removeCallback(mBouncerlessExpansionCallback);
mDreamOverlayAnimationsController.cancelAnimations();
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
index a49b3ae..c11c49c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
@@ -19,6 +19,7 @@
import android.os.Handler
import android.util.Log
+import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
@@ -57,21 +58,7 @@
TreeMap<String, KeyguardBlueprint>().apply { putAll(blueprints.associateBy { it.id }) }
val blueprint: MutableStateFlow<KeyguardBlueprint> = MutableStateFlow(blueprintIdMap[DEFAULT]!!)
val refreshTransition = MutableSharedFlow<Config>(extraBufferCapacity = 1)
- private var targetTransitionConfig: Config? = null
-
- /**
- * Emits the blueprint value to the collectors.
- *
- * @param blueprintId
- * @return whether the transition has succeeded.
- */
- fun applyBlueprint(index: Int): Boolean {
- ArrayList(blueprintIdMap.values)[index]?.let {
- applyBlueprint(it)
- return true
- }
- return false
- }
+ @VisibleForTesting var targetTransitionConfig: Config? = null
/**
* Emits the blueprint value to the collectors.
@@ -81,27 +68,21 @@
*/
fun applyBlueprint(blueprintId: String?): Boolean {
val blueprint = blueprintIdMap[blueprintId]
- return if (blueprint != null) {
- applyBlueprint(blueprint)
- true
- } else {
+ if (blueprint == null) {
Log.e(
TAG,
"Could not find blueprint with id: $blueprintId. " +
"Perhaps it was not added to KeyguardBlueprintModule?"
)
- false
+ return false
}
- }
- /** Emits the blueprint value to the collectors. */
- fun applyBlueprint(blueprint: KeyguardBlueprint?) {
if (blueprint == this.blueprint.value) {
- refreshBlueprint()
- return
+ return true
}
- blueprint?.let { this.blueprint.value = it }
+ this.blueprint.value = blueprint
+ return true
}
/**
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 e32bfcf..7f3274c 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
@@ -134,7 +134,7 @@
TransitionInfo(
ownerName = "",
from = KeyguardState.OFF,
- to = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.OFF,
animator = null
)
)
@@ -266,6 +266,14 @@
}
override suspend fun emitInitialStepsFromOff(to: KeyguardState) {
+ _currentTransitionInfo.value =
+ TransitionInfo(
+ ownerName = "KeyguardTransitionRepository(boot)",
+ from = KeyguardState.OFF,
+ to = to,
+ animator = null
+ )
+
emitTransition(
TransitionStep(
KeyguardState.OFF,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
index 5a28f711..9b07675f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -26,6 +26,7 @@
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
import com.android.wm.shell.animation.Interpolators
import javax.inject.Inject
@@ -140,6 +141,8 @@
}
private fun listenForAlternateBouncerToGone() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
if (KeyguardWmStateRefactor.isEnabled) {
// Handled via #dismissAlternateBouncer.
return
@@ -162,6 +165,8 @@
}
private fun listenForAlternateBouncerToPrimaryBouncer() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
scope.launch {
keyguardInteractor.primaryBouncerShowing
.filterRelevantKeyguardStateAnd { isPrimaryBouncerShowing ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index 4d73774..a306954 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -28,6 +28,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.Utils.Companion.sample
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -185,6 +186,7 @@
* PRIMARY_BOUNCER.
*/
private fun listenForAodToPrimaryBouncer() {
+ if (SceneContainerFlag.isEnabled) return
scope.launch("$TAG#listenForAodToPrimaryBouncer") {
keyguardInteractor.primaryBouncerShowing
.filterRelevantKeyguardStateAnd { primaryBouncerShowing -> primaryBouncerShowing }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
index e738ea4..63294f7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingLockscreenHostedTransitionInteractor.kt
@@ -26,6 +26,7 @@
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -93,6 +94,8 @@
}
private fun listenForDreamingLockscreenHostedToPrimaryBouncer() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
scope.launch {
keyguardInteractor.primaryBouncerShowing
.filterRelevantKeyguardStateAnd { isBouncerShowing -> isBouncerShowing }
@@ -101,6 +104,8 @@
}
private fun listenForDreamingLockscreenHostedToGone() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
scope.launch {
keyguardInteractor.biometricUnlockState
.filterRelevantKeyguardStateAnd { biometricUnlockState ->
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 c952e08..7961b45 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
@@ -29,6 +29,7 @@
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
@@ -88,6 +89,8 @@
private fun listenForDreamingToGlanceableHub() {
if (!communalHub()) return
+ if (SceneContainerFlag.isEnabled)
+ return // TODO(b/336576536): Check if adaptation for scene framework is needed
scope.launch("$TAG#listenForDreamingToGlanceableHub", mainDispatcher) {
glanceableHubTransitions.listenForGlanceableHubTransition(
transitionOwnerName = TAG,
@@ -175,6 +178,8 @@
}
private fun listenForDreamingToGoneWhenDismissable() {
+ if (SceneContainerFlag.isEnabled)
+ return // TODO(b/336576536): Check if adaptation for scene framework is needed
scope.launch {
keyguardInteractor.isAbleToDream
.sampleCombine(
@@ -190,6 +195,8 @@
}
private fun listenForDreamingToGoneFromBiometricUnlock() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
scope.launch {
keyguardInteractor.biometricUnlockState
.filterRelevantKeyguardStateAnd { biometricUnlockState ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index faab033..da4e989d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -28,6 +28,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import javax.inject.Inject
@@ -62,6 +63,8 @@
) {
override fun start() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
if (!Flags.communalHub()) {
return
}
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 c2c095b..2b3732f 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
@@ -29,6 +29,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -62,6 +63,8 @@
) {
override fun start() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
listenForGoneToAodOrDozing()
listenForGoneToDreaming()
listenForGoneToLockscreenOrHub()
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 56261e0..dad2d96 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
@@ -20,6 +20,7 @@
import android.util.MathUtils
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launch
+import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -32,6 +33,7 @@
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
import java.util.UUID
@@ -150,6 +152,7 @@
}
private fun listenForLockscreenToPrimaryBouncer() {
+ if (SceneContainerFlag.isEnabled) return
scope.launch("$TAG#listenForLockscreenToPrimaryBouncer") {
keyguardInteractor.primaryBouncerShowing
.filterRelevantKeyguardStateAnd { isBouncerShowing -> isBouncerShowing }
@@ -174,6 +177,7 @@
/* Starts transitions when manually dragging up the bouncer from the lockscreen. */
private fun listenForLockscreenToPrimaryBouncerDragging() {
+ if (SceneContainerFlag.isEnabled) return
var transitionId: UUID? = null
scope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") {
shadeRepository.legacyShadeExpansion
@@ -280,6 +284,7 @@
}
private fun listenForLockscreenToGoneDragging() {
+ if (SceneContainerFlag.isEnabled) return
if (KeyguardWmStateRefactor.isEnabled) {
// When the refactor is enabled, we no longer use isKeyguardGoingAway.
scope.launch("$TAG#listenForLockscreenToGoneDragging") {
@@ -337,7 +342,9 @@
* keyguard transition.
*/
private fun listenForLockscreenToGlanceableHub() {
- if (!com.android.systemui.Flags.communalHub()) {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
+ if (!Flags.communalHub()) {
return
}
scope.launch(mainDispatcher) {
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 e51ba83..9559250 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
@@ -26,6 +26,7 @@
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.Utils.Companion.sample
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
@@ -89,18 +90,10 @@
.filterRelevantKeyguardStateAnd { onTop -> !onTop }
.sample(
communalInteractor.isIdleOnCommunal,
- communalInteractor.showCommunalFromOccluded,
+ communalInteractor.showCommunalFromOccluded
)
.collect { (_, isIdleOnCommunal, showCommunalFromOccluded) ->
- // Occlusion signals come from the framework, and should interrupt any
- // existing transition
- val to =
- if (isIdleOnCommunal || showCommunalFromOccluded) {
- KeyguardState.GLANCEABLE_HUB
- } else {
- KeyguardState.LOCKSCREEN
- }
- startTransitionTo(to)
+ startTransitionToLockscreenOrHub(isIdleOnCommunal, showCommunalFromOccluded)
}
}
} else {
@@ -115,21 +108,28 @@
!isOccluded && isShowing
}
.collect { (_, _, isIdleOnCommunal, showCommunalFromOccluded) ->
- // Occlusion signals come from the framework, and should interrupt any
- // existing transition
- val to =
- if (isIdleOnCommunal || showCommunalFromOccluded) {
- KeyguardState.GLANCEABLE_HUB
- } else {
- KeyguardState.LOCKSCREEN
- }
- startTransitionTo(to)
+ startTransitionToLockscreenOrHub(isIdleOnCommunal, showCommunalFromOccluded)
}
}
}
}
+ private suspend fun FromOccludedTransitionInteractor.startTransitionToLockscreenOrHub(
+ isIdleOnCommunal: Boolean,
+ showCommunalFromOccluded: Boolean,
+ ) {
+ if (isIdleOnCommunal || showCommunalFromOccluded) {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
+ startTransitionTo(KeyguardState.GLANCEABLE_HUB)
+ } else {
+ startTransitionTo(KeyguardState.LOCKSCREEN)
+ }
+ }
+
private fun listenForOccludedToGone() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
if (KeyguardWmStateRefactor.isEnabled) {
// We don't think OCCLUDED to GONE is possible. You should always have to go via a
// *_BOUNCER state to end up GONE. Launching an activity over a dismissable keyguard
@@ -150,10 +150,6 @@
}
}
- fun dismissToGone() {
- scope.launch { startTransitionTo(KeyguardState.GONE) }
- }
-
private fun listenForOccludedToAsleep() {
scope.launch { listenForSleepTransition() }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index 181a551..53a0c32 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -28,6 +28,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.kotlin.Utils.Companion.sample
import com.android.systemui.util.kotlin.sample
@@ -98,6 +99,8 @@
}
private fun listenForPrimaryBouncerToLockscreenHubOrOccluded() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
if (KeyguardWmStateRefactor.isEnabled) {
scope.launch {
keyguardInteractor.primaryBouncerShowing
@@ -158,10 +161,14 @@
}
private fun listenForPrimaryBouncerToAsleep() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
scope.launch { listenForSleepTransition() }
}
private fun listenForPrimaryBouncerToDreamingLockscreenHosted() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
scope.launch {
keyguardInteractor.primaryBouncerShowing
.sample(keyguardInteractor.isActiveDreamLockscreenHosted, ::Pair)
@@ -174,6 +181,8 @@
}
private fun listenForPrimaryBouncerToGone() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
if (KeyguardWmStateRefactor.isEnabled) {
// This is handled in KeyguardSecurityContainerController and
// StatusBarKeyguardViewManager, which calls the transition interactor to kick off a
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
index 197221a..fcf67d5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
@@ -25,6 +25,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.sample
import java.util.UUID
import javax.inject.Inject
@@ -49,6 +50,8 @@
fromState: KeyguardState,
toState: KeyguardState,
) {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
val toScene =
if (fromState == KeyguardState.GLANCEABLE_HUB) {
CommunalScenes.Blank
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index da4f85e..857096e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -36,9 +36,12 @@
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
@SysUISingleton
@@ -64,12 +67,7 @@
/** Current BlueprintId */
val blueprintId =
- combine(
- configurationInteractor.onAnyConfigurationChange,
- fingerprintPropertyInteractor.propertiesInitialized.filter { it },
- clockInteractor.currentClock,
- shadeInteractor.shadeMode,
- ) { _, _, _, shadeMode ->
+ shadeInteractor.shadeMode.map { shadeMode ->
val useSplitShade = shadeMode == ShadeMode.Split && !ComposeLockscreen.isEnabled
when {
useSplitShade -> SplitShadeKeyguardBlueprint.ID
@@ -77,17 +75,29 @@
}
}
+ private val refreshEvents: Flow<Unit> =
+ merge(
+ configurationInteractor.onAnyConfigurationChange,
+ fingerprintPropertyInteractor.propertiesInitialized.filter { it }.map { Unit },
+ )
+
init {
applicationScope.launch { blueprintId.collect { transitionToBlueprint(it) } }
+ applicationScope.launch { refreshEvents.collect { refreshBlueprint() } }
}
/**
- * Transitions to a blueprint.
+ * Transitions to a blueprint, or refreshes it if already applied.
*
* @param blueprintId
* @return whether the transition has succeeded.
*/
- fun transitionToBlueprint(blueprintId: String): Boolean {
+ fun transitionOrRefreshBlueprint(blueprintId: String): Boolean {
+ if (blueprintId == blueprint.value.id) {
+ refreshBlueprint()
+ return true
+ }
+
return keyguardBlueprintRepository.applyBlueprint(blueprintId)
}
@@ -97,7 +107,7 @@
* @param blueprintId
* @return whether the transition has succeeded.
*/
- fun transitionToBlueprint(blueprintId: Int): Boolean {
+ fun transitionToBlueprint(blueprintId: String): Boolean {
return keyguardBlueprintRepository.applyBlueprint(blueprintId)
}
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 88367f4..2d7b737 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
@@ -55,7 +55,6 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -63,10 +62,10 @@
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combineTransform
+import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
@@ -179,12 +178,7 @@
isDreaming && isDozeOff(dozeTransitionModel.to)
}
.sample(powerInteractor.isAwake) { isAbleToDream, isAwake -> isAbleToDream && isAwake }
- .flatMapLatest { isAbleToDream ->
- flow {
- delay(50)
- emit(isAbleToDream)
- }
- }
+ .debounce(50L)
.distinctUntilChanged()
/** Whether the keyguard is showing or not. */
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 a18579d..2c05d49 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
@@ -28,11 +28,11 @@
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
-import com.android.systemui.keyguard.shared.model.KeyguardState.OFF
import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -356,6 +356,8 @@
* state.
*/
fun startDismissKeyguardTransition() {
+ // TODO(b/336576536): Check if adaptation for scene framework is needed
+ if (SceneContainerFlag.isEnabled) return
when (val startedState = startedKeyguardState.replayCache.last()) {
LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard()
PRIMARY_BOUNCER -> fromPrimaryBouncerTransitionInteractor.get().dismissPrimaryBouncer()
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 bb2eeb7..dc35e43 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
@@ -16,11 +16,16 @@
package com.android.systemui.keyguard.domain.interactor
+import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.shared.model.BiometricUnlockMode
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
+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.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor
+import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -42,6 +47,7 @@
fromBouncerInteractor: FromPrimaryBouncerTransitionInteractor,
fromAlternateBouncerInteractor: FromAlternateBouncerTransitionInteractor,
notificationLaunchAnimationInteractor: NotificationLaunchAnimationInteractor,
+ sceneInteractor: SceneInteractor,
) {
private val defaultSurfaceBehindVisibility =
transitionInteractor.finishedKeyguardState.map(::isSurfaceVisible)
@@ -103,21 +109,42 @@
* animation. This is used to keep the RemoteAnimationTarget alive until we're done using it.
*/
val usingKeyguardGoingAwayAnimation: Flow<Boolean> =
- combine(
- transitionInteractor.isInTransitionToState(KeyguardState.GONE),
- transitionInteractor.finishedKeyguardState,
- surfaceBehindInteractor.isAnimatingSurface,
- notificationLaunchAnimationInteractor.isLaunchAnimationRunning,
- ) { isInTransitionToGone, finishedState, isAnimatingSurface, notifLaunchRunning ->
- // Using the animation if we're animating it directly, or if the
- // ActivityLaunchAnimator is in the process of animating it.
- val animationsRunning = isAnimatingSurface || notifLaunchRunning
- // We may still be animating the surface after the keyguard is fully GONE, since
- // some animations (like the translation spring) are not tied directly to the
- // transition step amount.
- isInTransitionToGone || (finishedState == KeyguardState.GONE && animationsRunning)
- }
- .distinctUntilChanged()
+ if (SceneContainerFlag.isEnabled) {
+ combine(
+ sceneInteractor.transitionState,
+ surfaceBehindInteractor.isAnimatingSurface,
+ notificationLaunchAnimationInteractor.isLaunchAnimationRunning,
+ ) { transition, isAnimatingSurface, isLaunchAnimationRunning ->
+ // Using the animation if we're animating it directly, or if the
+ // ActivityLaunchAnimator is in the process of animating it.
+ val isAnyAnimationRunning = isAnimatingSurface || isLaunchAnimationRunning
+ // We may still be animating the surface after the keyguard is fully GONE, since
+ // some animations (like the translation spring) are not tied directly to the
+ // transition step amount.
+ transition.isTransitioning(to = Scenes.Gone) ||
+ (isAnyAnimationRunning &&
+ (transition.isIdle(Scenes.Gone) ||
+ transition.isTransitioning(from = Scenes.Gone)))
+ }
+ .distinctUntilChanged()
+ } else {
+ combine(
+ transitionInteractor.isInTransitionToState(KeyguardState.GONE),
+ transitionInteractor.finishedKeyguardState,
+ surfaceBehindInteractor.isAnimatingSurface,
+ notificationLaunchAnimationInteractor.isLaunchAnimationRunning,
+ ) { isInTransitionToGone, finishedState, isAnimatingSurface, notifLaunchRunning ->
+ // Using the animation if we're animating it directly, or if the
+ // ActivityLaunchAnimator is in the process of animating it.
+ val animationsRunning = isAnimatingSurface || notifLaunchRunning
+ // We may still be animating the surface after the keyguard is fully GONE, since
+ // some animations (like the translation spring) are not tied directly to the
+ // transition step amount.
+ isInTransitionToGone ||
+ (finishedState == KeyguardState.GONE && animationsRunning)
+ }
+ .distinctUntilChanged()
+ }
/**
* Whether the lockscreen is visible, from the Window Manager (WM) perspective.
@@ -127,28 +154,44 @@
* want to know if the AOD/clock/notifs/etc. are visible.
*/
val lockscreenVisibility: Flow<Boolean> =
- transitionInteractor.currentKeyguardState
- .sample(transitionInteractor.startedStepWithPrecedingStep, ::Pair)
- .map { (currentState, startedWithPrev) ->
- val startedFromStep = startedWithPrev?.previousValue
- val startedStep = startedWithPrev?.newValue
- val returningToGoneAfterCancellation =
- startedStep?.to == KeyguardState.GONE &&
- startedFromStep?.transitionState == TransitionState.CANCELED &&
- startedFromStep.from == KeyguardState.GONE
+ if (SceneContainerFlag.isEnabled) {
+ sceneInteractor.transitionState
+ .pairwise(ObservableTransitionState.Idle(Scenes.Lockscreen))
+ .map { (prevTransitionState, transitionState) ->
+ val isReturningToGoneAfterCancellation =
+ prevTransitionState.isTransitioning(from = Scenes.Gone) &&
+ transitionState.isTransitioning(to = Scenes.Gone)
+ val isNotOnGone =
+ !transitionState.isTransitioning(from = Scenes.Gone) &&
+ !transitionState.isIdle(Scenes.Gone)
- if (!returningToGoneAfterCancellation) {
- // By default, apply the lockscreen visibility of the current state.
- KeyguardState.lockscreenVisibleInState(currentState)
- } else {
- // If we're transitioning to GONE after a prior canceled transition from GONE,
- // then this is the camera launch transition from an asleep state back to GONE.
- // We don't want to show the lockscreen since we're aborting the lock and going
- // back to GONE.
- KeyguardState.lockscreenVisibleInState(KeyguardState.GONE)
+ isNotOnGone && !isReturningToGoneAfterCancellation
}
- }
- .distinctUntilChanged()
+ .distinctUntilChanged()
+ } else {
+ transitionInteractor.currentKeyguardState
+ .sample(transitionInteractor.startedStepWithPrecedingStep, ::Pair)
+ .map { (currentState, startedWithPrev) ->
+ val startedFromStep = startedWithPrev?.previousValue
+ val startedStep = startedWithPrev?.newValue
+ val returningToGoneAfterCancellation =
+ startedStep?.to == KeyguardState.GONE &&
+ startedFromStep?.transitionState == TransitionState.CANCELED &&
+ startedFromStep.from == KeyguardState.GONE
+
+ if (!returningToGoneAfterCancellation) {
+ // By default, apply the lockscreen visibility of the current state.
+ KeyguardState.lockscreenVisibleInState(currentState)
+ } else {
+ // If we're transitioning to GONE after a prior canceled transition from
+ // GONE, then this is the camera launch transition from an asleep state back
+ // to GONE. We don't want to show the lockscreen since we're aborting the
+ // lock and going back to GONE.
+ KeyguardState.lockscreenVisibleInState(KeyguardState.GONE)
+ }
+ }
+ .distinctUntilChanged()
+ }
/**
* Whether always-on-display (AOD) is visible when the lockscreen is visible, from window
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 4f00495..e2b66c5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -76,7 +76,7 @@
view,
HapticFeedbackConstants.CONFIRM,
)
- applicationScope.launch { viewModel.onLongPress() }
+ applicationScope.launch { viewModel.onUserInteraction() }
}
}
@@ -116,6 +116,17 @@
launch("$TAG#viewModel.accessibilityDelegateHint") {
viewModel.accessibilityDelegateHint.collect { hint ->
view.accessibilityHintType = hint
+ if (hint != DeviceEntryIconView.AccessibilityHintType.NONE) {
+ view.setOnClickListener {
+ vibratorHelper.performHapticFeedback(
+ view,
+ HapticFeedbackConstants.CONFIRM,
+ )
+ applicationScope.launch { viewModel.onUserInteraction() }
+ }
+ } else {
+ view.setOnClickListener(null)
+ }
}
}
launch("$TAG#viewModel.useBackgroundProtection") {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index ccc48b5..bda6438 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -36,7 +36,6 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
-import com.android.app.tracing.coroutines.launch
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
import com.android.systemui.Flags.newAodTransition
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
index 35b2598..200d30c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt
@@ -65,12 +65,12 @@
object : AccessibilityDelegate() {
private val accessibilityAuthenticateHint =
AccessibilityNodeInfo.AccessibilityAction(
- AccessibilityNodeInfoCompat.ACTION_LONG_CLICK,
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
resources.getString(R.string.accessibility_authenticate_hint)
)
private val accessibilityEnterHint =
AccessibilityNodeInfo.AccessibilityAction(
- AccessibilityNodeInfoCompat.ACTION_LONG_CLICK,
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
resources.getString(R.string.accessibility_enter_hint)
)
override fun onInitializeAccessibilityNodeInfo(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
index ce7ec0e..962cdf1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt
@@ -46,15 +46,14 @@
return
}
- if (
- arg.isDigitsOnly() && keyguardBlueprintInteractor.transitionToBlueprint(arg.toInt())
- ) {
- pw.println("Transition succeeded!")
- } else if (keyguardBlueprintInteractor.transitionToBlueprint(arg)) {
- pw.println("Transition succeeded!")
- } else {
- pw.println("Invalid argument! To see available blueprint ids, run:")
- pw.println("$ adb shell cmd statusbar blueprint help")
+ when {
+ arg.isDigitsOnly() -> pw.println("Invalid argument! Use string ids.")
+ keyguardBlueprintInteractor.transitionOrRefreshBlueprint(arg) ->
+ pw.println("Transition succeeded!")
+ else -> {
+ pw.println("Invalid argument! To see available blueprint ids, run:")
+ pw.println("$ adb shell cmd statusbar blueprint help")
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
index da2fcc4..53b2697 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt
@@ -19,6 +19,7 @@
import android.animation.FloatEvaluator
import android.animation.IntEvaluator
import com.android.keyguard.KeyguardViewController
+import com.android.systemui.accessibility.domain.interactor.AccessibilityInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
@@ -68,6 +69,7 @@
private val keyguardViewController: Lazy<KeyguardViewController>,
private val deviceEntryInteractor: DeviceEntryInteractor,
private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
+ private val accessibilityInteractor: AccessibilityInteractor,
@Application private val scope: CoroutineScope,
) {
val isUdfpsSupported: StateFlow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
@@ -232,7 +234,8 @@
}
}
val isVisible: Flow<Boolean> = deviceEntryViewAlpha.map { it > 0f }.distinctUntilChanged()
- val isLongPressEnabled: Flow<Boolean> =
+
+ private val isInteractive: Flow<Boolean> =
combine(
iconType,
isUdfpsSupported,
@@ -244,17 +247,24 @@
DeviceEntryIconView.IconType.NONE -> false
}
}
-
val accessibilityDelegateHint: Flow<DeviceEntryIconView.AccessibilityHintType> =
- combine(iconType, isLongPressEnabled) { deviceEntryStatus, longPressEnabled ->
- if (longPressEnabled) {
- deviceEntryStatus.toAccessibilityHintType()
+ accessibilityInteractor.isEnabled.flatMapLatest { touchExplorationEnabled ->
+ if (touchExplorationEnabled) {
+ combine(iconType, isInteractive) { iconType, isInteractive ->
+ if (isInteractive) {
+ iconType.toAccessibilityHintType()
+ } else {
+ DeviceEntryIconView.AccessibilityHintType.NONE
+ }
+ }
} else {
- DeviceEntryIconView.AccessibilityHintType.NONE
+ flowOf(DeviceEntryIconView.AccessibilityHintType.NONE)
}
}
- suspend fun onLongPress() {
+ val isLongPressEnabled: Flow<Boolean> = isInteractive
+
+ suspend fun onUserInteraction() {
if (SceneContainerFlag.isEnabled) {
deviceEntryInteractor.attemptDeviceEntry()
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 933065b..295b293 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -442,7 +442,7 @@
| PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.GET_SHARED_LIBRARY_FILES));
int resId = resources.getIdentifier(
- "gesture_blocking_activities", "array", recentsPackageName);
+ "back_gesture_blocking_activities", "array", recentsPackageName);
if (resId == 0) {
Log.e(TAG, "No resource found for gesture-blocking activities");
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
index 6fb5174..5720f76 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
@@ -125,7 +125,10 @@
public void setNumPages(int numPages) {
setVisibility(numPages > 1 ? View.VISIBLE : View.GONE);
- if (numPages == getChildCount()) {
+ int childCount = getChildCount();
+ // We're checking if the width needs to be updated as it's possible that the number of pages
+ // was changed while the page indicator was not visible, automatically skipping onMeasure.
+ if (numPages == childCount && calculateWidth(childCount) == getMeasuredWidth()) {
return;
}
if (mAnimating) {
@@ -295,6 +298,10 @@
}
}
+ private int calculateWidth(int numPages) {
+ return (mPageIndicatorWidth - mPageDotWidth) * (numPages - 1) + mPageDotWidth;
+ }
+
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int N = getChildCount();
@@ -309,7 +316,7 @@
for (int i = 0; i < N; i++) {
getChildAt(i).measure(widthChildSpec, heightChildSpec);
}
- int width = (mPageIndicatorWidth - mPageDotWidth) * (N - 1) + mPageDotWidth;
+ int width = calculateWidth(N);
setMeasuredDimension(width, mPageIndicatorHeight);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index b515ce0..278352c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -28,6 +28,7 @@
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel.DEBUG
import com.android.systemui.log.core.LogLevel.ERROR
+import com.android.systemui.log.core.LogLevel.INFO
import com.android.systemui.log.core.LogLevel.VERBOSE
import com.android.systemui.log.dagger.QSConfigLog
import com.android.systemui.log.dagger.QSLog
@@ -56,6 +57,9 @@
fun d(@CompileTimeConstant msg: String, arg: Any) {
buffer.log(TAG, DEBUG, { str1 = arg.toString() }, { "$msg: $str1" })
}
+ fun i(@CompileTimeConstant msg: String, arg: Any) {
+ buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" })
+ }
fun logTileAdded(tileSpec: String) {
buffer.log(TAG, DEBUG, { str1 = tileSpec }, { "[$str1] Tile added" })
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt
new file mode 100644
index 0000000..28c1fbf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepository.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.pipeline.data.repository.InstalledTilesComponentRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.settings.UserTracker
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class IconAndNameCustomRepository
+@Inject
+constructor(
+ private val installedTilesComponentRepository: InstalledTilesComponentRepository,
+ private val userTracker: UserTracker,
+ @Background private val backgroundContext: CoroutineContext,
+) {
+ /**
+ * Returns a list of the icon/labels for all available (installed and enabled) tile services.
+ *
+ * No order is guaranteed.
+ */
+ suspend fun getCustomTileData(): List<EditTileData> {
+ return withContext(backgroundContext) {
+ val installedTiles =
+ installedTilesComponentRepository.getInstalledTilesServiceInfos(userTracker.userId)
+ val packageManager = userTracker.userContext.packageManager
+ installedTiles
+ .map {
+ val tileSpec = TileSpec.create(it.componentName)
+ val label = it.loadLabel(packageManager)
+ val icon = it.loadIcon(packageManager)
+ val appName = it.applicationInfo.loadLabel(packageManager)
+ if (icon != null) {
+ EditTileData(
+ tileSpec,
+ Icon.Loaded(icon, ContentDescription.Loaded(label.toString())),
+ Text.Loaded(label.toString()),
+ Text.Loaded(appName.toString()),
+ )
+ } else {
+ null
+ }
+ }
+ .filterNotNull()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt
new file mode 100644
index 0000000..ec9d151
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/StockTilesRepository.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.content.res.Resources
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+@SysUISingleton
+class StockTilesRepository
+@Inject
+constructor(
+ @Main private val resources: Resources,
+) {
+ /**
+ * List of stock platform tiles. All of the specs will be of type [TileSpec.PlatformTileSpec].
+ */
+ val stockTiles =
+ resources
+ .getString(R.string.quick_settings_tiles_stock)
+ .split(",")
+ .map(TileSpec::create)
+ .filterNot { it is TileSpec.Invalid }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt
new file mode 100644
index 0000000..3b29422
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractor.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.data.repository.IconAndNameCustomRepository
+import com.android.systemui.qs.panels.data.repository.StockTilesRepository
+import com.android.systemui.qs.panels.domain.model.EditTilesModel
+import com.android.systemui.qs.panels.shared.model.EditTileData
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider
+import javax.inject.Inject
+
+@SysUISingleton
+class EditTilesListInteractor
+@Inject
+constructor(
+ private val stockTilesRepository: StockTilesRepository,
+ private val qsTileConfigProvider: QSTileConfigProvider,
+ private val iconAndNameCustomRepository: IconAndNameCustomRepository,
+) {
+ /**
+ * Provides a list of the tiles to edit, with their UI information (icon, labels).
+ *
+ * The icons have the label as their content description.
+ */
+ suspend fun getTilesToEdit(): EditTilesModel {
+ val stockTiles =
+ stockTilesRepository.stockTiles.map {
+ if (qsTileConfigProvider.hasConfig(it.spec)) {
+ val config = qsTileConfigProvider.getConfig(it.spec)
+ EditTileData(
+ it,
+ Icon.Resource(
+ config.uiConfig.iconRes,
+ ContentDescription.Resource(config.uiConfig.labelRes)
+ ),
+ Text.Resource(config.uiConfig.labelRes),
+ null,
+ )
+ } else {
+ EditTileData(
+ it,
+ Icon.Resource(
+ android.R.drawable.star_on,
+ ContentDescription.Loaded(it.spec)
+ ),
+ Text.Loaded(it.spec),
+ null
+ )
+ }
+ }
+ return EditTilesModel(stockTiles, iconAndNameCustomRepository.getCustomTileData())
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt
new file mode 100644
index 0000000..b573b9a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/model/EditTilesModel.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.model
+
+import com.android.systemui.qs.panels.shared.model.EditTileData
+
+data class EditTilesModel(
+ val stockTiles: List<EditTileData>,
+ val customTiles: List<EditTileData>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt
new file mode 100644
index 0000000..8b70bb9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/shared/model/EditTileData.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.shared.model
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+data class EditTileData(
+ val tileSpec: TileSpec,
+ val icon: Icon,
+ val label: Text,
+ val appName: Text?,
+) {
+ init {
+ check(
+ (tileSpec is TileSpec.PlatformTileSpec && appName == null) ||
+ (tileSpec is TileSpec.CustomTileSpec && appName != null)
+ ) {
+ "tileSpec: $tileSpec - appName: $appName. " +
+ "appName must be non-null for custom tiles and only for custom tiles."
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt
new file mode 100644
index 0000000..5c17fd1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/EditMode.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.panels.ui.compose
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
+
+@Composable
+fun EditMode(
+ viewModel: EditModeViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val gridLayout by viewModel.gridLayout.collectAsState()
+ val tiles by viewModel.tiles.collectAsState(emptyList())
+
+ BackHandler { viewModel.stopEditing() }
+
+ DisposableEffect(Unit) { onDispose { viewModel.stopEditing() } }
+
+ Column(modifier) {
+ gridLayout.EditTileGrid(
+ tiles,
+ Modifier,
+ viewModel::addTile,
+ viewModel::removeTile,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
index 68ce5d8..8806931 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/GridLayout.kt
@@ -18,7 +18,9 @@
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
interface GridLayout {
@Composable
@@ -26,4 +28,12 @@
tiles: List<TileViewModel>,
modifier: Modifier,
)
+
+ @Composable
+ fun EditTileGrid(
+ tiles: List<EditTileViewModel>,
+ modifier: Modifier,
+ onAddTile: (TileSpec, Int) -> Unit,
+ onRemoveTile: (TileSpec) -> Unit,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
index e2143e0..6539cf3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/InfiniteGridLayout.kt
@@ -28,6 +28,8 @@
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
@@ -37,8 +39,14 @@
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
+import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Remove
+import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -47,6 +55,7 @@
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -56,14 +65,28 @@
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.integerResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.unit.dp
+import com.android.compose.modifiers.background
import com.android.compose.theme.colorAttr
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.common.ui.compose.load
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.panels.domain.interactor.IconTilesInteractor
+import com.android.systemui.qs.panels.ui.viewmodel.ActiveTileColorAttributes
+import com.android.systemui.qs.panels.ui.viewmodel.AvailableEditActions
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.TileColorAttributes
import com.android.systemui.qs.panels.ui.viewmodel.TileUiState
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.toUiState
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
+import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.res.R
import javax.inject.Inject
@@ -75,6 +98,8 @@
class InfiniteGridLayout @Inject constructor(private val iconTilesInteractor: IconTilesInteractor) :
GridLayout {
+ private object TileType
+
@Composable
override fun TileGrid(
tiles: List<TileViewModel>,
@@ -88,17 +113,7 @@
val iconTilesSpecs by
iconTilesInteractor.iconTilesSpecs.collectAsState(initial = emptySet())
- LazyVerticalGrid(
- columns =
- GridCells.Fixed(
- integerResource(R.integer.quick_settings_infinite_grid_num_columns)
- ),
- verticalArrangement =
- Arrangement.spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
- horizontalArrangement =
- Arrangement.spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)),
- modifier = modifier
- ) {
+ TileLazyGrid(modifier) {
items(
tiles.size,
span = { index ->
@@ -131,29 +146,11 @@
.mapLatest { it.toUiState() }
.collectAsState(initial = tile.currentState.toUiState())
val context = LocalContext.current
- val horizontalAlignment =
- if (iconOnly) {
- Alignment.CenterHorizontally
- } else {
- Alignment.Start
- }
Row(
- modifier =
- modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)))
- .clickable { tile.onClick(null) }
- .background(colorAttr(state.colors.background))
- .padding(
- horizontal = dimensionResource(id = R.dimen.qs_label_container_margin)
- ),
+ modifier = modifier.clickable { tile.onClick(null) }.tileModifier(state.colors),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement =
- Arrangement.spacedBy(
- space = dimensionResource(id = R.dimen.qs_label_container_margin),
- alignment = horizontalAlignment
- )
+ horizontalArrangement = tileHorizontalArrangement(iconOnly)
) {
val icon =
remember(state.icon) {
@@ -165,62 +162,275 @@
}
}
}
- TileIcon(icon, colorAttr(state.colors.icon))
+ TileContent(
+ label = state.label.toString(),
+ secondaryLabel = state.secondaryLabel.toString(),
+ icon = icon,
+ colors = state.colors,
+ iconOnly = iconOnly
+ )
+ }
+ }
- if (!iconOnly) {
- Column(
- verticalArrangement = Arrangement.Center,
- modifier = Modifier.fillMaxHeight()
- ) {
- Text(
- state.label.toString(),
- color = colorAttr(state.colors.label),
- modifier = Modifier.basicMarquee(),
- )
- if (!TextUtils.isEmpty(state.secondaryLabel)) {
- Text(
- state.secondaryLabel.toString(),
- color = colorAttr(state.colors.secondaryLabel),
- modifier = Modifier.basicMarquee(),
- )
- }
+ @Composable
+ override fun EditTileGrid(
+ tiles: List<EditTileViewModel>,
+ modifier: Modifier,
+ onAddTile: (TileSpec, Int) -> Unit,
+ onRemoveTile: (TileSpec) -> Unit,
+ ) {
+ val (currentTiles, otherTiles) = tiles.partition { it.isCurrent }
+ val (otherTilesStock, otherTilesCustom) = otherTiles.partition { it.appName == null }
+ val addTileToEnd: (TileSpec) -> Unit by rememberUpdatedState {
+ onAddTile(it, POSITION_AT_END)
+ }
+ val iconOnlySpecs by iconTilesInteractor.iconTilesSpecs.collectAsState(initial = emptySet())
+ val isIconOnly: (TileSpec) -> Boolean =
+ remember(iconOnlySpecs) { { tileSpec: TileSpec -> tileSpec in iconOnlySpecs } }
+
+ TileLazyGrid(modifier = modifier) {
+ // These Text are just placeholders to see the different sections. Not final UI.
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ Text("Current tiles", color = Color.White)
+ }
+
+ editTiles(
+ currentTiles,
+ ClickAction.REMOVE,
+ onRemoveTile,
+ isIconOnly,
+ indicatePosition = true,
+ )
+
+ item(span = { GridItemSpan(maxLineSpan) }) { Text("Tiles to add", color = Color.White) }
+
+ editTiles(
+ otherTilesStock,
+ ClickAction.ADD,
+ addTileToEnd,
+ isIconOnly,
+ )
+
+ item(span = { GridItemSpan(maxLineSpan) }) {
+ Text("Custom tiles to add", color = Color.White)
+ }
+
+ editTiles(
+ otherTilesCustom,
+ ClickAction.ADD,
+ addTileToEnd,
+ isIconOnly,
+ )
+ }
+ }
+
+ private fun LazyGridScope.editTiles(
+ tiles: List<EditTileViewModel>,
+ clickAction: ClickAction,
+ onClick: (TileSpec) -> Unit,
+ isIconOnly: (TileSpec) -> Boolean,
+ indicatePosition: Boolean = false,
+ ) {
+ items(
+ count = tiles.size,
+ key = { tiles[it].tileSpec.spec },
+ span = { GridItemSpan(if (isIconOnly(tiles[it].tileSpec)) 1 else 2) },
+ contentType = { TileType }
+ ) {
+ val viewModel = tiles[it]
+ val canClick =
+ when (clickAction) {
+ ClickAction.ADD -> AvailableEditActions.ADD in viewModel.availableEditActions
+ ClickAction.REMOVE ->
+ AvailableEditActions.REMOVE in viewModel.availableEditActions
+ }
+ val onClickActionName =
+ when (clickAction) {
+ ClickAction.ADD ->
+ stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
+ ClickAction.REMOVE ->
+ stringResource(id = R.string.accessibility_qs_edit_remove_tile_action)
+ }
+ val stateDescription =
+ if (indicatePosition) {
+ stringResource(id = R.string.accessibility_qs_edit_position, it + 1)
+ } else {
+ ""
+ }
+
+ Box(
+ modifier =
+ Modifier.clickable(enabled = canClick) { onClick.invoke(viewModel.tileSpec) }
+ .animateItem()
+ .semantics {
+ onClick(onClickActionName) { false }
+ this.stateDescription = stateDescription
+ }
+ ) {
+ EditTile(
+ tileViewModel = viewModel,
+ isIconOnly(viewModel.tileSpec),
+ modifier = Modifier.height(dimensionResource(id = R.dimen.qs_tile_height))
+ )
+ if (canClick) {
+ Badge(clickAction, Modifier.align(Alignment.TopEnd))
}
}
}
}
- @OptIn(ExperimentalAnimationGraphicsApi::class)
@Composable
- private fun TileIcon(icon: Icon, color: Color) {
- val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
- val context = LocalContext.current
- val loadedDrawable =
- remember(icon, context) {
- when (icon) {
- is Icon.Loaded -> icon.drawable
- is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res)
- }
- }
- if (loadedDrawable !is Animatable) {
+ private fun Badge(action: ClickAction, modifier: Modifier = Modifier) {
+ Box(modifier = modifier.size(16.dp).background(Color.Cyan, shape = CircleShape)) {
Icon(
- icon = icon,
- tint = color,
- modifier = modifier,
+ imageVector =
+ when (action) {
+ ClickAction.ADD -> Icons.Filled.Add
+ ClickAction.REMOVE -> Icons.Filled.Remove
+ },
+ "",
+ tint = Color.Black,
)
- } else if (icon is Icon.Resource) {
- val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
- var atEnd by remember(icon.res) { mutableStateOf(false) }
- LaunchedEffect(key1 = icon.res) {
- delay(350)
- atEnd = true
+ }
+ }
+
+ @Composable
+ private fun EditTile(
+ tileViewModel: EditTileViewModel,
+ iconOnly: Boolean,
+ modifier: Modifier = Modifier,
+ ) {
+ val label = tileViewModel.label.load() ?: tileViewModel.tileSpec.spec
+ val colors = ActiveTileColorAttributes
+
+ Row(
+ modifier = modifier.tileModifier(colors).semantics { this.contentDescription = label },
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = tileHorizontalArrangement(iconOnly)
+ ) {
+ TileContent(
+ label = label,
+ secondaryLabel = tileViewModel.appName?.load(),
+ colors = colors,
+ icon = tileViewModel.icon,
+ iconOnly = iconOnly,
+ animateIconToEnd = true,
+ )
+ }
+ }
+
+ private enum class ClickAction {
+ ADD,
+ REMOVE,
+ }
+}
+
+@OptIn(ExperimentalAnimationGraphicsApi::class)
+@Composable
+private fun TileIcon(
+ icon: Icon,
+ color: Color,
+ animateToEnd: Boolean = false,
+) {
+ val modifier = Modifier.size(dimensionResource(id = R.dimen.qs_icon_size))
+ val context = LocalContext.current
+ val loadedDrawable =
+ remember(icon, context) {
+ when (icon) {
+ is Icon.Loaded -> icon.drawable
+ is Icon.Resource -> AppCompatResources.getDrawable(context, icon.res)
}
- val painter = rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
- Image(
- painter = painter,
- contentDescription = null,
- colorFilter = ColorFilter.tint(color = color),
- modifier = modifier
+ }
+ if (loadedDrawable !is Animatable) {
+ Icon(
+ icon = icon,
+ tint = color,
+ modifier = modifier,
+ )
+ } else if (icon is Icon.Resource) {
+ val image = AnimatedImageVector.animatedVectorResource(id = icon.res)
+ val painter =
+ if (animateToEnd) {
+ rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = true)
+ } else {
+ var atEnd by remember(icon.res) { mutableStateOf(false) }
+ LaunchedEffect(key1 = icon.res) {
+ delay(350)
+ atEnd = true
+ }
+ rememberAnimatedVectorPainter(animatedImageVector = image, atEnd = atEnd)
+ }
+ Image(
+ painter = painter,
+ contentDescription = null,
+ colorFilter = ColorFilter.tint(color = color),
+ modifier = modifier
+ )
+ }
+}
+
+@Composable
+private fun TileLazyGrid(
+ modifier: Modifier = Modifier,
+ content: LazyGridScope.() -> Unit,
+) {
+ LazyVerticalGrid(
+ columns =
+ GridCells.Fixed(integerResource(R.integer.quick_settings_infinite_grid_num_columns)),
+ verticalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)),
+ horizontalArrangement = spacedBy(dimensionResource(R.dimen.qs_tile_margin_horizontal)),
+ modifier = modifier,
+ content = content,
+ )
+}
+
+@Composable
+private fun Modifier.tileModifier(colors: TileColorAttributes): Modifier {
+ return fillMaxWidth()
+ .clip(RoundedCornerShape(dimensionResource(R.dimen.qs_corner_radius)))
+ .background(colorAttr(colors.background))
+ .padding(horizontal = dimensionResource(id = R.dimen.qs_label_container_margin))
+}
+
+@Composable
+private fun tileHorizontalArrangement(iconOnly: Boolean): Arrangement.Horizontal {
+ val horizontalAlignment =
+ if (iconOnly) {
+ Alignment.CenterHorizontally
+ } else {
+ Alignment.Start
+ }
+ return spacedBy(
+ space = dimensionResource(id = R.dimen.qs_label_container_margin),
+ alignment = horizontalAlignment
+ )
+}
+
+@Composable
+private fun TileContent(
+ label: String,
+ secondaryLabel: String?,
+ icon: Icon,
+ colors: TileColorAttributes,
+ iconOnly: Boolean,
+ animateIconToEnd: Boolean = false,
+) {
+ TileIcon(icon, colorAttr(colors.icon), animateIconToEnd)
+
+ if (!iconOnly) {
+ Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) {
+ Text(
+ label,
+ color = colorAttr(colors.label),
+ modifier = Modifier.basicMarquee(),
)
+ if (!TextUtils.isEmpty(secondaryLabel)) {
+ Text(
+ secondaryLabel ?: "",
+ color = colorAttr(colors.secondaryLabel),
+ modifier = Modifier.basicMarquee(),
+ )
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
new file mode 100644
index 0000000..69f50a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.panels.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.panels.domain.interactor.EditTilesListInteractor
+import com.android.systemui.qs.panels.domain.interactor.GridLayoutTypeInteractor
+import com.android.systemui.qs.panels.shared.model.GridLayoutType
+import com.android.systemui.qs.panels.ui.compose.GridLayout
+import com.android.systemui.qs.panels.ui.compose.InfiniteGridLayout
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
+import com.android.systemui.qs.pipeline.domain.interactor.MinimumTilesInteractor
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+@OptIn(ExperimentalCoroutinesApi::class)
+class EditModeViewModel
+@Inject
+constructor(
+ private val editTilesListInteractor: EditTilesListInteractor,
+ private val currentTilesInteractor: CurrentTilesInteractor,
+ private val minTilesInteractor: MinimumTilesInteractor,
+ private val defaultGridLayout: InfiniteGridLayout,
+ @Application private val applicationScope: CoroutineScope,
+ gridLayoutTypeInteractor: GridLayoutTypeInteractor,
+ gridLayoutMap: Map<GridLayoutType, @JvmSuppressWildcards GridLayout>,
+) {
+ private val _isEditing = MutableStateFlow(false)
+
+ /**
+ * Whether we should be editing right now. Use [startEditing] and [stopEditing] to change this
+ */
+ val isEditing = _isEditing.asStateFlow()
+ private val minimumTiles: Int
+ get() = minTilesInteractor.minNumberOfTiles
+
+ val gridLayout: StateFlow<GridLayout> =
+ gridLayoutTypeInteractor.layout
+ .map { gridLayoutMap[it] ?: defaultGridLayout }
+ .stateIn(
+ applicationScope,
+ SharingStarted.WhileSubscribed(),
+ defaultGridLayout,
+ )
+
+ /**
+ * Flow of view models for each tile that should be visible in edit mode (or empty flow when not
+ * editing).
+ *
+ * Guarantees of the data:
+ * * The data for the tiles is fetched once whenever [isEditing] goes from `false` to `true`.
+ * This prevents icons/labels changing while in edit mode.
+ * * It tracks the current tiles as they are added/removed/moved by the user.
+ * * The tiles that are current will be in the same relative order as the user sees them in
+ * Quick Settings.
+ * * The tiles that are not current will preserve their relative order even when the current
+ * tiles change.
+ */
+ val tiles =
+ isEditing.flatMapLatest {
+ if (it) {
+ val editTilesData = editTilesListInteractor.getTilesToEdit()
+ currentTilesInteractor.currentTiles.map { tiles ->
+ val currentSpecs = tiles.map { it.spec }
+ val canRemoveTiles = currentSpecs.size > minimumTiles
+ val allTiles = editTilesData.stockTiles + editTilesData.customTiles
+ val allTilesMap = allTiles.associate { it.tileSpec to it }
+ val currentTiles = currentSpecs.map { allTilesMap.get(it) }.filterNotNull()
+ val nonCurrentTiles = allTiles.filter { it.tileSpec !in currentSpecs }
+
+ (currentTiles + nonCurrentTiles).map {
+ val current = it.tileSpec in currentSpecs
+ val availableActions = buildSet {
+ if (current) {
+ add(AvailableEditActions.MOVE)
+ if (canRemoveTiles) {
+ add(AvailableEditActions.REMOVE)
+ }
+ } else {
+ add(AvailableEditActions.ADD)
+ }
+ }
+ EditTileViewModel(
+ it.tileSpec,
+ it.icon,
+ it.label,
+ it.appName,
+ current,
+ availableActions
+ )
+ }
+ }
+ } else {
+ emptyFlow()
+ }
+ }
+
+ /** @see isEditing */
+ fun startEditing() {
+ _isEditing.value = true
+ }
+
+ /** @see isEditing */
+ fun stopEditing() {
+ _isEditing.value = false
+ }
+
+ /** Immediately moves [tileSpec] to [position]. */
+ fun moveTile(tileSpec: TileSpec, position: Int) {
+ throw NotImplementedError("This is not supported yet")
+ }
+
+ /** Immediately adds [tileSpec] to the current tiles at [position]. */
+ fun addTile(tileSpec: TileSpec, position: Int = POSITION_AT_END) {
+ currentTilesInteractor.addTile(tileSpec, position)
+ }
+
+ /** Immediately removes [tileSpec] from the current tiles. */
+ fun removeTile(tileSpec: TileSpec) {
+ currentTilesInteractor.removeTiles(listOf(tileSpec))
+ }
+
+ /** Immediately resets the current tiles to the default list. */
+ fun resetCurrentTilesToDefault() {
+ throw NotImplementedError("This is not supported yet")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
new file mode 100644
index 0000000..ba9a044
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditTileViewModel.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.panels.ui.viewmodel
+
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.common.shared.model.Text
+import com.android.systemui.qs.pipeline.shared.TileSpec
+
+/**
+ * View model for each tile that is available to be added/removed/moved in Edit mode.
+ *
+ * [isCurrent] indicates whether this tile is part of the current set of tiles that the user sees in
+ * Quick Settings.
+ */
+class EditTileViewModel(
+ val tileSpec: TileSpec,
+ val icon: Icon,
+ val label: Text,
+ val appName: Text?,
+ val isCurrent: Boolean,
+ val availableEditActions: Set<AvailableEditActions>,
+)
+
+enum class AvailableEditActions {
+ ADD,
+ REMOVE,
+ MOVE,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
index cfcea98..c5b2737 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
@@ -23,6 +23,7 @@
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ServiceInfo
import android.os.UserHandle
import android.service.quicksettings.TileService
import androidx.annotation.GuardedBy
@@ -36,14 +37,17 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
interface InstalledTilesComponentRepository {
fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>>
+
+ fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo>
}
@SysUISingleton
@@ -55,38 +59,45 @@
private val packageChangeRepository: PackageChangeRepository
) : InstalledTilesComponentRepository {
- @GuardedBy("userMap") private val userMap = mutableMapOf<Int, Flow<Set<ComponentName>>>()
+ @GuardedBy("userMap") private val userMap = mutableMapOf<Int, StateFlow<List<ServiceInfo>>>()
override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> =
- synchronized(userMap) {
- userMap.getOrPut(userId) {
- /*
- * In order to query [PackageManager] for different users, this implementation will
- * call [Context.createContextAsUser] and retrieve the [PackageManager] from that
- * context.
- */
- val packageManager =
- if (applicationContext.userId == userId) {
- applicationContext.packageManager
- } else {
- applicationContext
- .createContextAsUser(
- UserHandle.of(userId),
- /* flags */ 0,
- )
- .packageManager
- }
- packageChangeRepository
- .packageChanged(UserHandle.of(userId))
- .onStart { emit(PackageChangeModel.Empty) }
- .map { reloadComponents(userId, packageManager) }
- .distinctUntilChanged()
- .shareIn(backgroundScope, SharingStarted.WhileSubscribed(), replay = 1)
- }
+ synchronized(userMap) { getForUserLocked(userId) }
+ .map { it.mapTo(mutableSetOf()) { it.componentName } }
+
+ override fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo> {
+ return synchronized(userMap) { getForUserLocked(userId).value }
+ }
+
+ private fun getForUserLocked(userId: Int): StateFlow<List<ServiceInfo>> {
+ return userMap.getOrPut(userId) {
+ /*
+ * In order to query [PackageManager] for different users, this implementation will
+ * call [Context.createContextAsUser] and retrieve the [PackageManager] from that
+ * context.
+ */
+ val packageManager =
+ if (applicationContext.userId == userId) {
+ applicationContext.packageManager
+ } else {
+ applicationContext
+ .createContextAsUser(
+ UserHandle.of(userId),
+ /* flags */ 0,
+ )
+ .packageManager
+ }
+ packageChangeRepository
+ .packageChanged(UserHandle.of(userId))
+ .onStart { emit(PackageChangeModel.Empty) }
+ .map { reloadComponents(userId, packageManager) }
+ .distinctUntilChanged()
+ .stateIn(backgroundScope, SharingStarted.WhileSubscribed(), emptyList())
}
+ }
@WorkerThread
- private fun reloadComponents(userId: Int, packageManager: PackageManager): Set<ComponentName> {
+ private fun reloadComponents(userId: Int, packageManager: PackageManager): List<ServiceInfo> {
return packageManager
.queryIntentServicesAsUser(INTENT, FLAGS, userId)
.mapNotNull { it.serviceInfo }
@@ -100,7 +111,6 @@
false
}
}
- .mapTo(mutableSetOf()) { it.componentName }
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 61896f0..b7fcef4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -115,6 +115,10 @@
* @see TileSpecRepository.setTiles
*/
fun setTiles(specs: List<TileSpec>)
+
+ companion object {
+ val POSITION_AT_END: Int = TileSpecRepository.POSITION_AT_END
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt
new file mode 100644
index 0000000..2ae3f07
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractor.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.pipeline.domain.interactor
+
+import com.android.systemui.qs.pipeline.data.repository.MinimumTilesRepository
+import javax.inject.Inject
+
+class MinimumTilesInteractor
+@Inject
+constructor(
+ private val minimumTilesRepository: MinimumTilesRepository,
+) {
+ val minNumberOfTiles: Int
+ get() = minimumTilesRepository.minNumberOfTiles
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index c24113f1..56588ff 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -55,6 +55,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.settingslib.graph.SignalDrawable;
import com.android.systemui.Dumpable;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.Expandable;
@@ -632,12 +633,23 @@
}
public static class DrawableIcon extends Icon {
+
protected final Drawable mDrawable;
protected final Drawable mInvisibleDrawable;
+ private static final String TAG = "QSTileImpl";
public DrawableIcon(Drawable drawable) {
mDrawable = drawable;
- mInvisibleDrawable = drawable.getConstantState().newDrawable();
+ Drawable.ConstantState nullableConstantState = drawable.getConstantState();
+ if (nullableConstantState == null) {
+ if (!(drawable instanceof SignalDrawable)) {
+ Log.w(TAG, "DrawableIcon: drawable has null ConstantState"
+ + " and is not a SignalDrawable");
+ }
+ mInvisibleDrawable = drawable;
+ } else {
+ mInvisibleDrawable = nullableConstantState.newDrawable();
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
index 065e89f..f0d7206 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/logging/QSTileLogger.kt
@@ -175,6 +175,26 @@
)
}
+ /** Log with level [LogLevel.WARNING] */
+ fun logWarning(
+ tileSpec: TileSpec,
+ message: String,
+ ) {
+ tileSpec
+ .getLogBuffer()
+ .log(tileSpec.getLogTag(), LogLevel.WARNING, { str1 = message }, { str1!! })
+ }
+
+ /** Log with level [LogLevel.INFO] */
+ fun logInfo(
+ tileSpec: TileSpec,
+ message: String,
+ ) {
+ tileSpec
+ .getLogBuffer()
+ .log(tileSpec.getLogTag(), LogLevel.INFO, { str1 = message }, { str1!! })
+ }
+
fun logCustomTileUserActionDelivered(tileSpec: TileSpec) {
tileSpec
.getLogBuffer()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 60469c0..b057476 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -16,6 +16,8 @@
package com.android.systemui.qs.tiles.dialog;
+import static android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING;
+
import static com.android.settingslib.mobile.MobileMappings.getIconKey;
import static com.android.settingslib.mobile.MobileMappings.mapIconSets;
import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource;
@@ -190,7 +192,7 @@
private DialogTransitionAnimator mDialogTransitionAnimator;
private boolean mHasWifiEntries;
private WifiStateWorker mWifiStateWorker;
- private boolean mHasActiveSubId;
+ private boolean mHasActiveSubIdOnDds;
@VisibleForTesting
static final float TOAST_PARAMS_HORIZONTAL_WEIGHT = 1.0f;
@@ -298,7 +300,7 @@
mExecutor);
// Listen the subscription changes
mOnSubscriptionsChangedListener = new InternetOnSubscriptionChangedListener();
- refreshHasActiveSubId();
+ refreshHasActiveSubIdOnDds();
mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor,
mOnSubscriptionsChangedListener);
mDefaultDataSubId = getDefaultDataSubscriptionId();
@@ -428,7 +430,7 @@
}
boolean isActiveOnNonDds = getActiveAutoSwitchNonDdsSubId() != SubscriptionManager
.INVALID_SUBSCRIPTION_ID;
- if (!hasActiveSubId() || (!isVoiceStateInService(mDefaultDataSubId)
+ if (!hasActiveSubIdOnDds() || (!isVoiceStateInService(mDefaultDataSubId)
&& !isDataStateInService(mDefaultDataSubId) && !isActiveOnNonDds)) {
if (DEBUG) {
Log.d(TAG, "No carrier or service is out of service.");
@@ -901,23 +903,42 @@
/**
* @return whether there is the carrier item in the slice.
*/
- boolean hasActiveSubId() {
+ boolean hasActiveSubIdOnDds() {
if (isAirplaneModeEnabled() || mTelephonyManager == null) {
return false;
}
- return mHasActiveSubId;
+ return mHasActiveSubIdOnDds;
}
- private void refreshHasActiveSubId() {
+ private static boolean isEmbeddedSubscriptionVisible(@NonNull SubscriptionInfo subInfo) {
+ if (subInfo.isEmbedded() && subInfo.getProfileClass() == PROFILE_CLASS_PROVISIONING) {
+ return false;
+ }
+ return true;
+ }
+
+ private void refreshHasActiveSubIdOnDds() {
if (mSubscriptionManager == null) {
- mHasActiveSubId = false;
+ mHasActiveSubIdOnDds = false;
Log.e(TAG, "SubscriptionManager is null, set mHasActiveSubId = false");
return;
}
+ int dds = getDefaultDataSubscriptionId();
+ if (dds == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ mHasActiveSubIdOnDds = false;
+ Log.d(TAG, "DDS is INVALID_SUBSCRIPTION_ID");
+ return;
+ }
+ SubscriptionInfo ddsSubInfo = mSubscriptionManager.getActiveSubscriptionInfo(dds);
+ if (ddsSubInfo == null) {
+ mHasActiveSubIdOnDds = false;
+ Log.e(TAG, "Can't get DDS subscriptionInfo");
+ return;
+ }
- mHasActiveSubId = mSubscriptionManager.getActiveSubscriptionIdList().length > 0;
- Log.i(TAG, "mHasActiveSubId:" + mHasActiveSubId);
+ mHasActiveSubIdOnDds = isEmbeddedSubscriptionVisible(ddsSubInfo);
+ Log.i(TAG, "mHasActiveSubId:" + mHasActiveSubIdOnDds);
}
/**
@@ -1209,7 +1230,7 @@
@Override
public void onSubscriptionsChanged() {
- refreshHasActiveSubId();
+ refreshHasActiveSubIdOnDds();
updateListener();
}
}
@@ -1306,6 +1327,7 @@
Log.d(TAG, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED");
}
mConfig = MobileMappings.Config.readConfig(context);
+ refreshHasActiveSubIdOnDds();
updateListener();
} else if (WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION.equals(action)) {
updateListener();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
index 1a881b6..c9c4443 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java
@@ -429,7 +429,7 @@
}
boolean isWifiEnabled = mInternetDialogController.isWifiEnabled();
- if (!mInternetDialogController.hasActiveSubId()
+ if (!mInternetDialogController.hasActiveSubIdOnDds()
&& (!isWifiEnabled || !isCarrierNetworkActive)) {
mMobileNetworkLayout.setVisibility(View.GONE);
if (mSecondaryMobileNetworkLayout != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
index d1c8030..bd2f2c9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/interactor/LocationTileDataInteractor.kt
@@ -17,15 +17,15 @@
package com.android.systemui.qs.tiles.impl.location.domain.interactor
import android.os.UserHandle
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.impl.location.domain.model.LocationTileModel
import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.util.kotlin.isLocationEnabledFlow
import javax.inject.Inject
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
/** Observes location state changes providing the [LocationTileModel]. */
class LocationTileDataInteractor
@@ -38,19 +38,7 @@
user: UserHandle,
triggers: Flow<DataUpdateTrigger>
): Flow<LocationTileModel> =
- ConflatedCallbackFlow.conflatedCallbackFlow {
- val initialValue = locationController.isLocationEnabled
- trySend(LocationTileModel(initialValue))
-
- val callback =
- object : LocationController.LocationChangeCallback {
- override fun onLocationSettingsChanged(locationEnabled: Boolean) {
- trySend(LocationTileModel(locationEnabled))
- }
- }
- locationController.addCallback(callback)
- awaitClose { locationController.removeCallback(callback) }
- }
+ locationController.isLocationEnabledFlow().map { LocationTileModel(it) }
override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt
new file mode 100644
index 0000000..88bd224
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileDataInteractor.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.interactor
+
+import android.content.Context
+import android.hardware.display.ColorDisplayManager
+import android.os.UserHandle
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.util.time.DateFormatUtil
+import java.time.LocalTime
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/** Observes screen record state changes providing the [NightDisplayTileModel]. */
+class NightDisplayTileDataInteractor
+@Inject
+constructor(
+ @Application private val context: Context,
+ private val dateFormatUtil: DateFormatUtil,
+ private val nightDisplayRepository: NightDisplayRepository,
+) : QSTileDataInteractor<NightDisplayTileModel> {
+
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<NightDisplayTileModel> =
+ nightDisplayRepository.nightDisplayState(user).map {
+ generateModel(
+ it.autoMode,
+ it.isActivated,
+ it.startTime,
+ it.endTime,
+ it.shouldForceAutoMode,
+ it.locationEnabled
+ )
+ }
+
+ /** This checks resources and there fore does not make a binder call. */
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ flowOf(ColorDisplayManager.isNightDisplayAvailable(context))
+
+ private fun generateModel(
+ autoMode: Int,
+ isNightDisplayActivated: Boolean,
+ customStartTime: LocalTime?,
+ customEndTime: LocalTime?,
+ shouldForceAutoMode: Boolean,
+ locationEnabled: Boolean,
+ ): NightDisplayTileModel {
+ if (autoMode == ColorDisplayManager.AUTO_MODE_TWILIGHT) {
+ return NightDisplayTileModel.AutoModeTwilight(
+ isNightDisplayActivated,
+ shouldForceAutoMode,
+ locationEnabled,
+ )
+ } else if (autoMode == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME) {
+ return NightDisplayTileModel.AutoModeCustom(
+ isNightDisplayActivated,
+ shouldForceAutoMode,
+ customStartTime,
+ customEndTime,
+ dateFormatUtil.is24HourFormat,
+ )
+ } else { // auto mode off
+ return NightDisplayTileModel.AutoModeOff(isNightDisplayActivated, shouldForceAutoMode)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
new file mode 100644
index 0000000..5cee8c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/interactor/NightDisplayTileUserActionInteractor.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.interactor
+
+import android.content.Intent
+import android.hardware.display.ColorDisplayManager.AUTO_MODE_CUSTOM_TIME
+import android.provider.Settings
+import com.android.systemui.accessibility.data.repository.NightDisplayRepository
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import javax.inject.Inject
+
+/** Handles night display tile clicks. */
+class NightDisplayTileUserActionInteractor
+@Inject
+constructor(
+ private val nightDisplayRepository: NightDisplayRepository,
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
+ private val qsLogger: QSTileLogger,
+) : QSTileUserActionInteractor<NightDisplayTileModel> {
+ override suspend fun handleInput(input: QSTileInput<NightDisplayTileModel>): Unit =
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click -> {
+ // Enroll in forced auto mode if eligible.
+ if (data.isEnrolledInForcedNightDisplayAutoMode) {
+ nightDisplayRepository.setNightDisplayAutoMode(AUTO_MODE_CUSTOM_TIME, user)
+ qsLogger.logInfo(spec, "Enrolled in forced night display auto mode")
+ }
+ nightDisplayRepository.setNightDisplayActivated(!data.isActivated, user)
+ }
+ is QSTileUserAction.LongClick -> {
+ qsTileIntentUserActionHandler.handle(
+ action.expandable,
+ Intent(Settings.ACTION_NIGHT_DISPLAY_SETTINGS)
+ )
+ }
+ }
+ }
+
+ companion object {
+ val spec = TileSpec.create(QSAccessibilityModule.NIGHT_DISPLAY_TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt
new file mode 100644
index 0000000..6b1bd5b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/domain/model/NightDisplayTileModel.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.domain.model
+
+import java.time.LocalTime
+
+/** Data model for night display tile */
+sealed interface NightDisplayTileModel {
+ val isActivated: Boolean
+ val isEnrolledInForcedNightDisplayAutoMode: Boolean
+ data class AutoModeTwilight(
+ override val isActivated: Boolean,
+ override val isEnrolledInForcedNightDisplayAutoMode: Boolean,
+ val isLocationEnabled: Boolean
+ ) : NightDisplayTileModel
+ data class AutoModeCustom(
+ override val isActivated: Boolean,
+ override val isEnrolledInForcedNightDisplayAutoMode: Boolean,
+ val startTime: LocalTime?,
+ val endTime: LocalTime?,
+ val is24HourFormat: Boolean
+ ) : NightDisplayTileModel
+ data class AutoModeOff(
+ override val isActivated: Boolean,
+ override val isEnrolledInForcedNightDisplayAutoMode: Boolean
+ ) : NightDisplayTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
new file mode 100644
index 0000000..5c2dcfc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night.ui
+
+import android.content.res.Resources
+import android.service.quicksettings.Tile
+import android.text.TextUtils
+import androidx.annotation.StringRes
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.base.logging.QSTileLogger
+import com.android.systemui.qs.tiles.impl.night.domain.model.NightDisplayTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import java.time.DateTimeException
+import java.time.LocalTime
+import java.time.format.DateTimeFormatter
+import javax.inject.Inject
+
+/** Maps [NightDisplayTileModel] to [QSTileState]. */
+class NightDisplayTileMapper
+@Inject
+constructor(
+ @Main private val resources: Resources,
+ private val theme: Resources.Theme,
+ private val logger: QSTileLogger,
+) : QSTileDataToStateMapper<NightDisplayTileModel> {
+ override fun map(config: QSTileConfig, data: NightDisplayTileModel): QSTileState =
+ QSTileState.build(resources, theme, config.uiConfig) {
+ label = resources.getString(R.string.quick_settings_night_display_label)
+ supportedActions =
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK)
+ sideViewIcon = QSTileState.SideViewIcon.None
+
+ if (data.isActivated) {
+ activationState = QSTileState.ActivationState.ACTIVE
+ val loadedIcon =
+ Icon.Loaded(
+ resources.getDrawable(R.drawable.qs_nightlight_icon_on, theme),
+ contentDescription = null
+ )
+ icon = { loadedIcon }
+ } else {
+ activationState = QSTileState.ActivationState.INACTIVE
+ val loadedIcon =
+ Icon.Loaded(
+ resources.getDrawable(R.drawable.qs_nightlight_icon_off, theme),
+ contentDescription = null
+ )
+ icon = { loadedIcon }
+ }
+
+ secondaryLabel = getSecondaryLabel(data, resources)
+
+ contentDescription =
+ if (TextUtils.isEmpty(secondaryLabel)) label
+ else TextUtils.concat(label, ", ", secondaryLabel)
+ }
+
+ private fun getSecondaryLabel(
+ data: NightDisplayTileModel,
+ resources: Resources
+ ): CharSequence? {
+ when (data) {
+ is NightDisplayTileModel.AutoModeTwilight -> {
+ if (!data.isLocationEnabled) {
+ return null
+ } else {
+ return resources.getString(
+ if (data.isActivated)
+ R.string.quick_settings_night_secondary_label_until_sunrise
+ else R.string.quick_settings_night_secondary_label_on_at_sunset
+ )
+ }
+ }
+ is NightDisplayTileModel.AutoModeOff -> {
+ val subtitleArray = resources.getStringArray(R.array.tile_states_night)
+ return subtitleArray[
+ if (data.isActivated) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE]
+ }
+ is NightDisplayTileModel.AutoModeCustom -> {
+ // User-specified time, approximated to the nearest hour.
+ @StringRes val toggleTimeStringRes: Int
+ val toggleTime: LocalTime
+ if (data.isActivated) {
+ toggleTime = data.endTime ?: return null
+ toggleTimeStringRes = R.string.quick_settings_secondary_label_until
+ } else {
+ toggleTime = data.startTime ?: return null
+ toggleTimeStringRes = R.string.quick_settings_night_secondary_label_on_at
+ }
+
+ try {
+ val formatter = if (data.is24HourFormat) formatter24Hour else formatter12Hour
+ val formatArg = formatter.format(toggleTime)
+ return resources.getString(toggleTimeStringRes, formatArg)
+ } catch (exception: DateTimeException) {
+ logger.logWarning(spec, exception.message.toString())
+ return null
+ }
+ }
+ }
+ }
+
+ private companion object {
+ val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("hh:mm a")
+ val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")
+ val spec = TileSpec.create(QSAccessibilityModule.NIGHT_DISPLAY_TILE_SPEC)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index b88c1e5..5346b23 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -201,6 +201,7 @@
qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) }
override fun getInstanceId(): InstanceId = qsTileViewModel.config.instanceId
+
override fun getTileLabel(): CharSequence =
with(qsTileViewModel.config.uiConfig) {
when (this) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
index d6325c0..a04fa38 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
@@ -18,6 +18,7 @@
import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel
import javax.inject.Inject
@@ -27,4 +28,5 @@
constructor(
val brightnessSliderViewModel: BrightnessSliderViewModel,
val tileGridViewModel: TileGridViewModel,
+ val editModeViewModel: EditModeViewModel,
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
index b91dd04..0603d21 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKeys.kt
@@ -24,6 +24,8 @@
* These are the subset of transitions that can be referenced by key when asking for a scene change.
*/
object TransitionKeys {
+ /** Reference to the gone to shade transition with split shade enabled. */
+ val GoneToSplitShade = TransitionKey("GoneToSplitShade")
/** Reference to a scene transition that can collapse the shade scene instantly. */
val CollapseShadeInstantly = TransitionKey("CollapseShadeInstantly")
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 259a8bf..b971781 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
@@ -56,9 +56,8 @@
}
// TODO(b/298525212): remove once Compose exposes window inset bounds.
- override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? {
- val insets = super.onApplyWindowInsets(windowInsets)
- this.windowInsets.value = insets
- return insets
+ override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets {
+ this.windowInsets.value = windowInsets
+ return windowInsets
}
}
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 78704e1..c20d577 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
@@ -198,7 +198,7 @@
private fun getDisplayWidth(context: Context): Dp {
val point = Point()
checkNotNull(context.display).getRealSize(point)
- return point.x.dp
+ return point.x.toDp(context)
}
// TODO(b/298525212): remove once Compose exposes window inset bounds.
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
index b0af7f9..016fe57 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt
@@ -24,6 +24,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
import javax.inject.Inject
@@ -70,10 +71,11 @@
)] = UserActionResult(Scenes.QuickSettingsShade)
}
+ val downSceneKey =
+ if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
+ val downTransitionKey = GoneToSplitShade.takeIf { shadeMode is ShadeMode.Split }
this[Swipe(direction = SwipeDirection.Down)] =
- UserActionResult(
- if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade
- )
+ UserActionResult(downSceneKey, downTransitionKey)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index ac76bec..d15a488 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -33,6 +33,7 @@
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.GoneToSplitShade
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
@@ -152,11 +153,13 @@
else -> Scenes.Lockscreen
}
+ val upTransitionKey = GoneToSplitShade.takeIf { shadeMode is ShadeMode.Split }
+
val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single }
return buildMap {
if (!isCustomizing) {
- this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+ this[Swipe(SwipeDirection.Up)] = UserActionResult(up, upTransitionKey)
} // TODO(b/330200163) Add an else to be able to collapse the shade while customizing
down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 70632d5..79218ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -18,6 +18,7 @@
import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_TO_AOD;
+import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE;
import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows;
import android.animation.Animator;
@@ -49,6 +50,7 @@
import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus;
import com.android.systemui.keyguard.MigrateClocksToBlueprint;
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
import com.android.systemui.res.R;
import com.android.systemui.scene.domain.interactor.SceneInteractor;
@@ -108,6 +110,7 @@
private final UiEventLogger mUiEventLogger;
private final Lazy<InteractionJankMonitor> mInteractionJankMonitorLazy;
private final JavaAdapter mJavaAdapter;
+ private final Lazy<KeyguardTransitionInteractor> mKeyguardTransitionInteractorLazy;
private final Lazy<ShadeInteractor> mShadeInteractorLazy;
private final Lazy<DeviceUnlockedInteractor> mDeviceUnlockedInteractorLazy;
private final Lazy<SceneInteractor> mSceneInteractorLazy;
@@ -175,6 +178,7 @@
UiEventLogger uiEventLogger,
Lazy<InteractionJankMonitor> interactionJankMonitorLazy,
JavaAdapter javaAdapter,
+ Lazy<KeyguardTransitionInteractor> keyguardTransitionInteractor,
Lazy<ShadeInteractor> shadeInteractorLazy,
Lazy<DeviceUnlockedInteractor> deviceUnlockedInteractorLazy,
Lazy<SceneInteractor> sceneInteractorLazy,
@@ -182,6 +186,7 @@
mUiEventLogger = uiEventLogger;
mInteractionJankMonitorLazy = interactionJankMonitorLazy;
mJavaAdapter = javaAdapter;
+ mKeyguardTransitionInteractorLazy = keyguardTransitionInteractor;
mShadeInteractorLazy = shadeInteractorLazy;
mDeviceUnlockedInteractorLazy = deviceUnlockedInteractorLazy;
mSceneInteractorLazy = sceneInteractorLazy;
@@ -193,6 +198,14 @@
@Override
public void start() {
+ mJavaAdapter.alwaysCollectFlow(
+ mKeyguardTransitionInteractorLazy.get().isFinishedInState(GONE),
+ (Boolean isFinishedInState) -> {
+ if (isFinishedInState) {
+ setLeaveOpenOnKeyguardHide(false);
+ }
+ });
+
mJavaAdapter.alwaysCollectFlow(mShadeInteractorLazy.get().isAnyExpanded(),
this::onShadeOrQsExpanded);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index c17da4b..0524589 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -32,6 +32,7 @@
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpHandler;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
@@ -56,10 +57,10 @@
import com.android.systemui.statusbar.phone.CentralSurfacesImpl;
import com.android.systemui.statusbar.phone.ManagedProfileController;
import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl;
-import com.android.systemui.statusbar.phone.ui.StatusBarIconList;
import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback;
import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
import com.android.systemui.statusbar.phone.ui.StatusBarIconControllerImpl;
+import com.android.systemui.statusbar.phone.ui.StatusBarIconList;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import dagger.Binds;
@@ -209,14 +210,16 @@
/** */
@Provides
@SysUISingleton
- static ActivityTransitionAnimator provideActivityTransitionAnimator() {
- return new ActivityTransitionAnimator();
+ static ActivityTransitionAnimator provideActivityTransitionAnimator(
+ @Main Executor mainExecutor) {
+ return new ActivityTransitionAnimator(mainExecutor);
}
/** */
@Provides
@SysUISingleton
- static DialogTransitionAnimator provideDialogTransitionAnimator(IDreamManager dreamManager,
+ static DialogTransitionAnimator provideDialogTransitionAnimator(@Main Executor mainExecutor,
+ IDreamManager dreamManager,
KeyguardStateController keyguardStateController,
Lazy<AlternateBouncerInteractor> alternateBouncerInteractor,
InteractionJankMonitor interactionJankMonitor,
@@ -243,7 +246,7 @@
}
};
return new DialogTransitionAnimator(
- callback, interactionJankMonitor, animationFeatureFlags);
+ mainExecutor, callback, interactionJankMonitor, animationFeatureFlags);
}
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
index 0c341cc..ec3c7d0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
@@ -27,6 +27,9 @@
* (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
*/
public interface NotificationActivityStarter {
+ /** Called when the user clicks on the notification bubble icon. */
+ void onNotificationBubbleIconClicked(NotificationEntry entry);
+
/** Called when the user clicks on the surface of a notification. */
void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
index d10fac6..6487d55 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java
@@ -117,11 +117,14 @@
Notification notification = sbn.getNotification();
if (notification.contentIntent != null || notification.fullScreenIntent != null
|| row.getEntry().isBubble()) {
+ row.setBubbleClickListener(v ->
+ mNotificationActivityStarter.onNotificationBubbleIconClicked(row.getEntry()));
row.setOnClickListener(this);
row.setOnDragSuccessListener(mOnDragSuccessListener);
} else {
row.setOnClickListener(null);
row.setOnDragSuccessListener(null);
+ row.setBubbleClickListener(null);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 5e3df7b..23c0a0d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -375,6 +375,8 @@
};
private OnClickListener mOnClickListener;
+ @Nullable
+ private OnClickListener mBubbleClickListener;
private OnDragSuccessListener mOnDragSuccessListener;
private boolean mHeadsupDisappearRunning;
private View mChildAfterViewWhenDismissed;
@@ -1234,14 +1236,19 @@
/**
* The click listener for the bubble button.
*/
+ @Nullable
public View.OnClickListener getBubbleClickListener() {
- return v -> {
- if (mBubblesManagerOptional.isPresent()) {
- mBubblesManagerOptional.get()
- .onUserChangedBubble(mEntry, !mEntry.isBubble() /* createBubble */);
- }
- mHeadsUpManager.removeNotification(mEntry.getKey(), true /* releaseImmediately */);
- };
+ return mBubbleClickListener;
+ }
+
+ /**
+ * Sets the click listener for the bubble button.
+ */
+ public void setBubbleClickListener(@Nullable OnClickListener l) {
+ mBubbleClickListener = l;
+ // ensure listener is passed to the content views
+ mPrivateLayout.updateBubbleButton(mEntry);
+ mPublicLayout.updateBubbleButton(mEntry);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt
index 816e5c1..db3cf5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProvider.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.row
import android.app.Flags
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
import javax.inject.Inject
/**
@@ -27,11 +28,14 @@
fun shouldApplyCompactStyle(): Boolean
}
-class HeadsUpStyleProviderImpl @Inject constructor() : HeadsUpStyleProvider {
+class HeadsUpStyleProviderImpl
+@Inject
+constructor(private val statusBarModeRepositoryStore: StatusBarModeRepositoryStore) :
+ HeadsUpStyleProvider {
- /**
- * TODO(b/270709257) This feature is under development. This method returns Compact when the
- * flag is enabled for fish fooding purpose.
- */
- override fun shouldApplyCompactStyle(): Boolean = Flags.compactHeadsUpNotification()
+ override fun shouldApplyCompactStyle(): Boolean {
+ // Use compact HUN for immersive mode.
+ return Flags.compactHeadsUpNotification() &&
+ statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 773a6bf..232b4e9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1469,9 +1469,10 @@
public void setExpandedHeight(float height) {
final boolean skipHeightUpdate = shouldSkipHeightUpdate();
- // when scene framework is enabled, updateStackPosition is already called by
- // updateTopPadding every time the stack moves, so skip it here to avoid flickering.
- if (!SceneContainerFlag.isEnabled()) {
+ // when scene framework is enabled and in single shade, updateStackPosition is already
+ // called by updateTopPadding every time the stack moves, so skip it here to avoid
+ // flickering.
+ if (!SceneContainerFlag.isEnabled() || mShouldUseSplitNotificationShade) {
updateStackPosition();
}
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 be6bef7..23674b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2184,7 +2184,9 @@
}
if (mStatusBarStateController.leaveOpenOnKeyguardHide()) {
if (!mStatusBarStateController.isKeyguardRequested()) {
- mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
+ if (!MigrateClocksToBlueprint.isEnabled()) {
+ mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
+ }
}
long delay = mKeyguardStateController.calculateGoingToFullShadeDelay();
mLockscreenShadeTransitionController.onHideKeyguard(delay, previousState);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index e1a7f22..e92058b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -96,6 +96,20 @@
@SysUISingleton
public class StatusBarNotificationActivityStarter implements NotificationActivityStarter {
+ /**
+ * Helps to avoid recalculation of values provided to
+ * {@link #onDismiss(PendingIntent, boolean, boolean, boolean)}} method
+ */
+ private interface OnKeyguardDismissedAction {
+ /**
+ * Invoked when keyguard is dismissed
+ *
+ * @return is used as return value for {@link ActivityStarter.OnDismissAction#onDismiss()}
+ */
+ boolean onDismiss(PendingIntent intent, boolean isActivityIntent, boolean animate,
+ boolean showOverTheLockScreen);
+ }
+
private final Context mContext;
private final int mDisplayId;
@@ -207,6 +221,30 @@
}
/**
+ * Called when the user clicks on the notification bubble icon.
+ *
+ * @param entry notification that bubble icon was clicked
+ */
+ @Override
+ public void onNotificationBubbleIconClicked(NotificationEntry entry) {
+ Runnable action = () -> {
+ mBubblesManagerOptional.ifPresent(bubblesManager ->
+ bubblesManager.onUserChangedBubble(entry, !entry.isBubble()));
+ mHeadsUpManager.removeNotification(entry.getKey(), /* releaseImmediately= */ true);
+ };
+ if (entry.isBubble()) {
+ // entry is being un-bubbled, no need to unlock
+ action.run();
+ } else {
+ performActionAfterKeyguardDismissed(entry,
+ (intent, isActivityIntent, animate, showOverTheLockScreen) -> {
+ action.run();
+ return false;
+ });
+ }
+ }
+
+ /**
* Called when a notification is clicked.
*
* @param entry notification that was clicked
@@ -217,7 +255,15 @@
mLogger.logStartingActivityFromClick(entry, row.isHeadsUpState(),
mKeyguardStateController.isVisible(),
mNotificationShadeWindowController.getPanelExpanded());
+ OnKeyguardDismissedAction action =
+ (intent, isActivityIntent, animate, showOverTheLockScreen) ->
+ performActionOnKeyguardDismissed(entry, row, intent, isActivityIntent,
+ animate, showOverTheLockScreen);
+ performActionAfterKeyguardDismissed(entry, action);
+ }
+ private void performActionAfterKeyguardDismissed(NotificationEntry entry,
+ OnKeyguardDismissedAction action) {
if (mRemoteInputManager.isRemoteInputActive(entry)) {
// We have an active remote input typed and the user clicked on the notification.
// this was probably unintentional, so we're closing the edit text instead.
@@ -251,8 +297,7 @@
ActivityStarter.OnDismissAction postKeyguardAction = new ActivityStarter.OnDismissAction() {
@Override
public boolean onDismiss() {
- return handleNotificationClickAfterKeyguardDismissed(
- entry, row, intent, isActivityIntent, animate, showOverLockscreen);
+ return action.onDismiss(intent, isActivityIntent, animate, showOverLockscreen);
}
@Override
@@ -271,7 +316,7 @@
}
}
- private boolean handleNotificationClickAfterKeyguardDismissed(
+ private boolean performActionOnKeyguardDismissed(
NotificationEntry entry,
ExpandableNotificationRow row,
PendingIntent intent,
@@ -282,7 +327,6 @@
final Runnable runnable = () -> handleNotificationClickAfterPanelCollapsed(
entry, row, intent, isActivityIntent, animate);
-
if (showOverLockscreen) {
mShadeController.addPostCollapseAction(runnable);
mShadeController.collapseShade(true /* animate */);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 226a84a..88ca9e5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -229,7 +229,7 @@
@SysUISingleton
@OemSatelliteInputLog
fun provideOemSatelliteInputLog(factory: LogBufferFactory): LogBuffer {
- return factory.create("DeviceBasedSatelliteInputLog", 32)
+ return factory.create("DeviceBasedSatelliteInputLog", 150)
}
const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
index d9d909a..fc54f14 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SubscriptionModel.kt
@@ -33,6 +33,18 @@
*/
val isOpportunistic: Boolean = false,
+ /**
+ * True if this subscription **only** supports non-terrestrial networks (NTN) and false
+ * otherwise. (non-terrestrial == satellite)
+ *
+ * Note that we intend to filter these subscriptions out, because these connections are actually
+ * supported by
+ * [com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository]. See
+ * [com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor] for
+ * the filtering.
+ */
+ val isExclusivelyNonTerrestrial: Boolean = false,
+
/** Subscriptions in the same group may be filtered or treated as a single subscription */
val groupUuid: ParcelUuid? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
index 2278597..425c58b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt
@@ -23,6 +23,7 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import kotlinx.coroutines.flow.StateFlow
@@ -76,7 +77,17 @@
*/
val isInService: StateFlow<Boolean>
- /** Reflects [android.telephony.ServiceState.isUsingNonTerrestrialNetwork] */
+ /**
+ * True if this subscription is actively connected to a non-terrestrial network and false
+ * otherwise. Reflects [android.telephony.ServiceState.isUsingNonTerrestrialNetwork].
+ *
+ * Notably: This value reflects that this subscription is **currently** using a non-terrestrial
+ * network, because some subscriptions can switch between terrestrial and non-terrestrial
+ * networks. [SubscriptionModel.isExclusivelyNonTerrestrial] reflects whether a subscription is
+ * configured to exclusively connect to non-terrestrial networks. [isNonTerrestrial] can change
+ * during the lifetime of a subscription but [SubscriptionModel.isExclusivelyNonTerrestrial]
+ * will stay constant.
+ */
val isNonTerrestrial: StateFlow<Boolean>
/** True if [android.telephony.SignalStrength] told us that this connection is using GSM */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index 5d91ef3..0073e9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -424,6 +424,7 @@
SubscriptionModel(
subscriptionId = subscriptionId,
isOpportunistic = isOpportunistic,
+ isExclusivelyNonTerrestrial = isOnlyNonTerrestrialNetwork,
groupUuid = groupUuid,
carrierName = carrierName.toString(),
profileClass = profileClass,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index d555c47..91d7ca6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -172,21 +172,33 @@
private val unfilteredSubscriptions: Flow<List<SubscriptionModel>> =
mobileConnectionsRepo.subscriptions
- /**
- * Any filtering that we can do based purely on the info of each subscription. Currently this
- * only applies the ProfileClass-based filter, but if we need other they can go here
- */
+ /** Any filtering that we can do based purely on the info of each subscription individually. */
private val subscriptionsBasedFilteredSubs =
- unfilteredSubscriptions.map { subs -> applyProvisioningFilter(subs) }.distinctUntilChanged()
+ unfilteredSubscriptions
+ .map { it.filterBasedOnProvisioning().filterBasedOnNtn() }
+ .distinctUntilChanged()
- private fun applyProvisioningFilter(subs: List<SubscriptionModel>): List<SubscriptionModel> =
+ private fun List<SubscriptionModel>.filterBasedOnProvisioning(): List<SubscriptionModel> =
if (!featureFlagsClassic.isEnabled(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS)) {
- subs
+ this
} else {
- subs.filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
+ this.filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
}
/**
+ * Subscriptions that exclusively support non-terrestrial networks should **never** directly
+ * show any iconography in the status bar. These subscriptions only exist to provide a backing
+ * for the device-based satellite connections, and the iconography for those connections are
+ * already being handled in
+ * [com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository]. We
+ * need to filter out those subscriptions here so we guarantee the subscription never turns into
+ * an icon. See b/336881301.
+ */
+ private fun List<SubscriptionModel>.filterBasedOnNtn(): List<SubscriptionModel> {
+ return this.filter { !it.isExclusivelyNonTerrestrial }
+ }
+
+ /**
* Generally, SystemUI wants to show iconography for each subscription that is listed by
* [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only
* show a single representation of the pair of subscriptions. The docs define opportunistic as:
@@ -204,12 +216,8 @@
subscriptionsBasedFilteredSubs,
mobileConnectionsRepo.activeMobileDataSubscriptionId,
connectivityRepository.vcnSubId,
- ) { unfilteredSubs, activeId, vcnSubId ->
- filterSubsBasedOnOpportunistic(
- unfilteredSubs,
- activeId,
- vcnSubId,
- )
+ ) { preFilteredSubs, activeId, vcnSubId ->
+ filterSubsBasedOnOpportunistic(preFilteredSubs, activeId, vcnSubId)
}
.distinctUntilChanged()
.logDiffsForTable(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index a7c4187..12f252d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -249,11 +249,17 @@
try {
sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
registered = true
+ logBuffer.i { "Registered for signal strength successfully" }
} catch (e: Exception) {
logBuffer.e("error registering for signal strength", e)
}
- awaitClose { if (registered) sm.unregisterForNtnSignalStrengthChanged(cb) }
+ awaitClose {
+ if (registered) {
+ sm.unregisterForNtnSignalStrengthChanged(cb)
+ logBuffer.i { "Unregistered for signal strength successfully" }
+ }
+ }
}
.flowOn(bgDispatcher)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
index 2670a95..fa8a7d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -253,6 +253,7 @@
if (nextList.isEmpty()) {
log { "NO MORE TO SHOW" }
+ previousHunKey = ""
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt
new file mode 100644
index 0000000..ee1b565
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/LocationControllerExt.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import com.android.systemui.statusbar.policy.LocationController
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.onStart
+
+fun LocationController.isLocationEnabledFlow(): Flow<Boolean> {
+ return conflatedCallbackFlow {
+ val locationCallback =
+ object : LocationController.LocationChangeCallback {
+ override fun onLocationSettingsChanged(locationEnabled: Boolean) {
+ trySend(locationEnabled)
+ }
+ }
+ addCallback(locationCallback)
+ awaitClose { removeCallback(locationCallback) }
+ }
+ .onStart { emit(isLocationEnabled) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index 0386338..c08cd64 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -49,6 +49,11 @@
private val uiEventLogger: UiEventLogger,
) : SliderViewModel {
+ private val streamsAffectedByRing =
+ setOf(
+ AudioManager.STREAM_RING,
+ AudioManager.STREAM_NOTIFICATION,
+ )
private val audioStream = audioStreamWrapper.audioStream
private val iconsByStream =
mapOf(
@@ -125,15 +130,42 @@
isEnabled: Boolean,
ringerMode: RingerMode,
): State {
+ val label =
+ labelsByStream[audioStream]?.let(context::getString)
+ ?: error("No label for the stream: $audioStream")
return State(
value = volume.toFloat(),
valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(),
icon = getIcon(ringerMode),
- label = labelsByStream[audioStream]?.let(context::getString)
- ?: error("No label for the stream: $audioStream"),
+ label = label,
disabledMessage = disabledTextByStream[audioStream]?.let(context::getString),
isEnabled = isEnabled,
a11yStep = volumeRange.step,
+ a11yClickDescription =
+ context.getString(
+ if (isMuted) {
+ R.string.volume_panel_hint_unmute
+ } else {
+ R.string.volume_panel_hint_mute
+ },
+ label,
+ ),
+ a11yStateDescription =
+ if (volume == volumeRange.first) {
+ context.getString(
+ if (audioStream.value in streamsAffectedByRing) {
+ if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
+ R.string.volume_panel_hint_vibrate
+ } else {
+ R.string.volume_panel_hint_muted
+ }
+ } else {
+ R.string.volume_panel_hint_muted
+ }
+ )
+ } else {
+ null
+ },
audioStreamModel = this,
isMutable = audioVolumeInteractor.isAffectedByMute(audioStream),
)
@@ -143,27 +175,14 @@
val isMutedOrNoVolume = isMuted || volume == minVolume
val iconRes =
if (isMutedOrNoVolume) {
- when (audioStream.value) {
- AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_off
- AudioManager.STREAM_BLUETOOTH_SCO -> R.drawable.ic_volume_off
- AudioManager.STREAM_VOICE_CALL -> R.drawable.ic_volume_off
- AudioManager.STREAM_RING ->
- if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
- R.drawable.ic_volume_ringer_vibrate
- } else {
- R.drawable.ic_volume_off
- }
- AudioManager.STREAM_NOTIFICATION ->
- if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
- R.drawable.ic_volume_ringer_vibrate
- } else {
- R.drawable.ic_volume_off
- }
- AudioManager.STREAM_ALARM -> R.drawable.ic_volume_off
- else -> {
- Log.wtf(TAG, "No icon for the stream: $audioStream")
+ if (audioStream.value in streamsAffectedByRing) {
+ if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
+ R.drawable.ic_volume_ringer_vibrate
+ } else {
R.drawable.ic_volume_off
}
+ } else {
+ R.drawable.ic_volume_off
}
} else {
iconsByStream[audioStream]
@@ -186,6 +205,8 @@
override val disabledMessage: String?,
override val isEnabled: Boolean,
override val a11yStep: Int,
+ override val a11yClickDescription: String?,
+ override val a11yStateDescription: String?,
override val isMutable: Boolean,
val audioStreamModel: AudioStreamModel,
) : SliderState
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index 956ab66..10714d1 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -68,7 +68,7 @@
icon = Icon.Resource(R.drawable.ic_cast, null),
label = context.getString(R.string.media_device_cast),
isEnabled = true,
- a11yStep = 1
+ a11yStep = 1,
)
}
@@ -85,6 +85,12 @@
override val isMutable: Boolean
get() = false
+
+ override val a11yClickDescription: String?
+ get() = null
+
+ override val a11yStateDescription: String?
+ get() = null
}
@AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
index d71a9d8..c951928 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -34,6 +34,8 @@
* enough to trigger rounding to the correct value.
*/
val a11yStep: Int
+ val a11yClickDescription: String?
+ val a11yStateDescription: String?
val disabledMessage: String?
val isMutable: Boolean
@@ -44,6 +46,8 @@
override val label: String = ""
override val disabledMessage: String? = null
override val a11yStep: Int = 0
+ override val a11yClickDescription: String? = null
+ override val a11yStateDescription: String? = null
override val isEnabled: Boolean = true
override val isMutable: Boolean = false
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index e72027a..6f550ba 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -371,7 +371,7 @@
}
@Test
- fun listenForTransitionToLSFromOccluded_updatesClockDozeAmountToOne() =
+ fun listenForTransitionToLSFromOccluded_updatesClockDozeAmountToZero() =
runBlocking(IMMEDIATE) {
val transitionStep = MutableStateFlow(TransitionStep())
whenever(keyguardTransitionInteractor.transitionStepsToState(KeyguardState.LOCKSCREEN))
@@ -434,6 +434,27 @@
}
@Test
+ fun listenForAnyStateToDozingTransition_UpdatesClockDozeAmountToOne() =
+ runBlocking(IMMEDIATE) {
+ val transitionStep = MutableStateFlow(TransitionStep())
+ whenever(keyguardTransitionInteractor.transitionStepsToState(KeyguardState.DOZING))
+ .thenReturn(transitionStep)
+
+ val job = underTest.listenForAnyStateToDozingTransition(this)
+ transitionStep.value =
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DOZING,
+ transitionState = TransitionState.STARTED,
+ )
+ yield()
+
+ verify(animations, times(2)).doze(1f)
+
+ job.cancel()
+ }
+
+ @Test
fun unregisterListeners_validate() =
runBlocking(IMMEDIATE) {
underTest.unregisterListeners()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index 41974f4..8e4c155 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -46,7 +46,8 @@
@RunWithLooper
class ActivityTransitionAnimatorTest : SysuiTestCase() {
private val transitionContainer = LinearLayout(mContext)
- private val testTransitionAnimator = fakeTransitionAnimator()
+ private val mainExecutor = context.mainExecutor
+ private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor)
@Mock lateinit var callback: ActivityTransitionAnimator.Callback
@Mock lateinit var listener: ActivityTransitionAnimator.Listener
@Spy private val controller = TestTransitionAnimatorController(transitionContainer)
@@ -59,9 +60,10 @@
fun setup() {
activityTransitionAnimator =
ActivityTransitionAnimator(
+ mainExecutor,
testTransitionAnimator,
testTransitionAnimator,
- disableWmTimeout = true
+ disableWmTimeout = true,
)
activityTransitionAnimator.callback = callback
activityTransitionAnimator.addListener(listener)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
index d84a578..e14762cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt
@@ -156,6 +156,7 @@
fun testActivityLaunchWhenLockedWithoutAlternateAuth() {
val dialogTransitionAnimator =
fakeDialogTransitionAnimator(
+ mainExecutor = mContext.mainExecutor,
isUnlocked = false,
isShowingAlternateAuthOnUnlock = false,
interactionJankMonitor = kosmos.interactionJankMonitor)
@@ -166,6 +167,7 @@
@Test
fun testActivityLaunchWhenLockedWithAlternateAuth() {
val dialogTransitionAnimator = fakeDialogTransitionAnimator(
+ mainExecutor = mContext.mainExecutor,
isUnlocked = false,
isShowingAlternateAuthOnUnlock = true,
interactionJankMonitor = kosmos.interactionJankMonitor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
index e64df90..259ece9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt
@@ -25,6 +25,8 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.activity.EmptyTestActivity
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -58,9 +60,11 @@
)
}
+ private val kosmos = Kosmos()
private val pathManager = GoldenPathManager(context, GOLDENS_PATH, pathConfig = PathConfig())
private val transitionAnimator =
TransitionAnimator(
+ kosmos.fakeExecutor,
ActivityTransitionAnimator.TIMINGS,
ActivityTransitionAnimator.INTERPOLATORS
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt
index 5caa146..0d01472 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/DefaultUdfpsTouchOverlayViewModelTest.kt
@@ -16,97 +16,95 @@
package com.android.systemui.biometrics.ui.viewmodel
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
-import com.android.systemui.SysUITestComponent
-import com.android.systemui.SysUITestModule
import com.android.systemui.SysuiTestCase
-import com.android.systemui.TestMocksModule
-import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
-import com.android.systemui.collectLastValue
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.BrokenWithSceneContainer
import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.flags.parameterizeSceneContainerFlag
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.runCurrent
-import com.android.systemui.runTest
-import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.phone.SystemUIDialogManager
-import com.android.systemui.user.domain.UserDomainLayerModule
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.statusbar.phone.systemUIDialogManager
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import dagger.BindsInstance
-import dagger.Component
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
-@RunWith(JUnit4::class)
-class DefaultUdfpsTouchOverlayViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class DefaultUdfpsTouchOverlayViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+
+ private val kosmos =
+ testKosmos().apply {
+ fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, true) }
+ }
+ private val testScope = kosmos.testScope
+
@Captor
private lateinit var sysuiDialogListenerCaptor: ArgumentCaptor<SystemUIDialogManager.Listener>
- private var systemUIDialogManager: SystemUIDialogManager = mock()
+ private var systemUIDialogManager = kosmos.systemUIDialogManager
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+ private lateinit var underTest: DefaultUdfpsTouchOverlayViewModel
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return parameterizeSceneContainerFlag()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ underTest =
+ DefaultUdfpsTouchOverlayViewModel(
+ kosmos.shadeInteractor,
+ systemUIDialogManager,
+ )
}
- @SysUISingleton
- @Component(
- modules =
- [
- SysUITestModule::class,
- UserDomainLayerModule::class,
- BiometricsDomainLayerModule::class,
- ]
- )
- interface TestComponent : SysUITestComponent<DefaultUdfpsTouchOverlayViewModel> {
- val keyguardRepository: FakeKeyguardRepository
- val shadeRepository: FakeShadeRepository
- @Component.Factory
- interface Factory {
- fun create(
- @BindsInstance test: SysuiTestCase,
- featureFlags: FakeFeatureFlagsClassicModule,
- mocks: TestMocksModule,
- ): TestComponent
- }
- }
-
- private fun TestComponent.shadeExpanded(expanded: Boolean) {
+ private fun shadeExpanded(expanded: Boolean) {
if (expanded) {
- shadeRepository.setLegacyShadeExpansion(1f)
- shadeRepository.setLegacyShadeTracking(false)
- shadeRepository.setLegacyExpandedOrAwaitingInputTransfer(true)
+ shadeTestUtil.setShadeExpansion(1f)
+ shadeTestUtil.setTracking(false)
+ shadeTestUtil.setLegacyExpandedOrAwaitingInputTransfer(true)
} else {
keyguardRepository.setStatusBarState(StatusBarState.SHADE)
- shadeRepository.setLegacyShadeExpansion(0f)
- shadeRepository.setLegacyShadeTracking(false)
- shadeRepository.setLegacyExpandedOrAwaitingInputTransfer(false)
+ shadeTestUtil.setShadeExpansion(0f)
+ shadeTestUtil.setTracking(false)
+ shadeTestUtil.setLegacyExpandedOrAwaitingInputTransfer(false)
}
}
- private val testComponent: TestComponent =
- DaggerDefaultUdfpsTouchOverlayViewModelTest_TestComponent.factory()
- .create(
- test = this,
- featureFlags =
- FakeFeatureFlagsClassicModule { set(Flags.FULL_SCREEN_USER_SWITCHER, true) },
- mocks = TestMocksModule(systemUIDialogManager = systemUIDialogManager),
- )
-
@Test
+ @BrokenWithSceneContainer(339465026)
fun shadeNotExpanded_noDialogShowing_shouldHandleTouchesTrue() =
- testComponent.runTest {
+ testScope.runTest {
val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
runCurrent()
@@ -120,7 +118,7 @@
@Test
fun shadeNotExpanded_dialogShowing_shouldHandleTouchesFalse() =
- testComponent.runTest {
+ testScope.runTest {
val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
runCurrent()
@@ -134,7 +132,7 @@
@Test
fun shadeExpanded_noDialogShowing_shouldHandleTouchesFalse() =
- testComponent.runTest {
+ testScope.runTest {
val shouldHandleTouches by collectLastValue(underTest.shouldHandleTouches)
runCurrent()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
index 28cbcb4..4bcd9a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
@@ -17,7 +17,7 @@
package com.android.systemui.bluetooth.qsdialog
import android.bluetooth.BluetoothDevice
-import android.content.pm.PackageInfo
+import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.media.AudioManager
import android.platform.test.annotations.DisableFlags
@@ -25,7 +25,6 @@
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
-import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.flags.Flags
import com.android.systemui.SysuiTestCase
@@ -120,11 +119,10 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
fun testSavedFactory_isFilterMatched_exclusivelyManaged_returnsFalse() {
- val exclusiveManagerName =
- BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME
`when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
- .thenReturn(exclusiveManagerName.toByteArray())
- `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)).thenReturn(PackageInfo())
+ .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+ `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
+ .thenReturn(ApplicationInfo())
`when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
`when`(cachedDevice.isConnected).thenReturn(false)
@@ -144,11 +142,11 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
- fun testSavedFactory_isFilterMatched_notAllowedExclusiveManager_returnsTrue() {
+ fun testSavedFactory_isFilterMatched_exclusiveManagerNotEnabled_returnsTrue() {
`when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
- .thenReturn(FAKE_EXCLUSIVE_MANAGER_NAME.toByteArray())
- `when`(packageManager.getPackageInfo(FAKE_EXCLUSIVE_MANAGER_NAME, 0))
- .thenReturn(PackageInfo())
+ .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+ `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
+ .thenReturn(ApplicationInfo().also { it.enabled = false })
`when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
`when`(cachedDevice.isConnected).thenReturn(false)
@@ -158,12 +156,10 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
- fun testSavedFactory_isFilterMatched_uninstalledExclusiveManager_returnsTrue() {
- val exclusiveManagerName =
- BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME
+ fun testSavedFactory_isFilterMatched_exclusiveManagerNotInstalled_returnsTrue() {
`when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
- .thenReturn(exclusiveManagerName.toByteArray())
- `when`(packageManager.getPackageInfo(exclusiveManagerName, 0))
+ .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+ `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
.thenThrow(PackageManager.NameNotFoundException("Test!"))
`when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
`when`(cachedDevice.isConnected).thenReturn(false)
@@ -228,11 +224,10 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
fun testConnectedFactory_isFilterMatched_exclusivelyManaged_returnsFalse() {
- val exclusiveManagerName =
- BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME
`when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
- .thenReturn(exclusiveManagerName.toByteArray())
- `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)).thenReturn(PackageInfo())
+ .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+ `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
+ .thenReturn(ApplicationInfo())
`when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
`when`(bluetoothDevice.isConnected).thenReturn(true)
audioManager.setMode(AudioManager.MODE_NORMAL)
@@ -254,11 +249,11 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
- fun testConnectedFactory_isFilterMatched_notAllowedExclusiveManager_returnsTrue() {
+ fun testConnectedFactory_isFilterMatched_exclusiveManagerNotEnabled_returnsTrue() {
`when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
- .thenReturn(FAKE_EXCLUSIVE_MANAGER_NAME.toByteArray())
- `when`(packageManager.getPackageInfo(FAKE_EXCLUSIVE_MANAGER_NAME, 0))
- .thenReturn(PackageInfo())
+ .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+ `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
+ .thenReturn(ApplicationInfo().also { it.enabled = false })
`when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
`when`(bluetoothDevice.isConnected).thenReturn(true)
audioManager.setMode(AudioManager.MODE_NORMAL)
@@ -269,12 +264,10 @@
@Test
@EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE)
- fun testConnectedFactory_isFilterMatched_uninstalledExclusiveManager_returnsTrue() {
- val exclusiveManagerName =
- BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME
+ fun testConnectedFactory_isFilterMatched_exclusiveManagerNotInstalled_returnsTrue() {
`when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER))
- .thenReturn(exclusiveManagerName.toByteArray())
- `when`(packageManager.getPackageInfo(exclusiveManagerName, 0))
+ .thenReturn(TEST_EXCLUSIVE_MANAGER.toByteArray())
+ `when`(packageManager.getApplicationInfo(TEST_EXCLUSIVE_MANAGER, 0))
.thenThrow(PackageManager.NameNotFoundException("Test!"))
`when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED)
`when`(bluetoothDevice.isConnected).thenReturn(true)
@@ -317,7 +310,7 @@
companion object {
const val DEVICE_NAME = "DeviceName"
const val CONNECTION_SUMMARY = "ConnectionSummary"
- private const val FAKE_EXCLUSIVE_MANAGER_NAME = "com.fake.name"
+ private const val TEST_EXCLUSIVE_MANAGER = "com.test.manager"
private const val DEVICE_ADDRESS = "04:52:C7:0B:D8:3C"
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
index bed05ee..cde7a0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
@@ -30,7 +30,6 @@
import java.nio.charset.Charset
import org.junit.After
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -102,12 +101,6 @@
assertThat(dataRead).isEqualTo(newDataToWrite)
}
- @Ignore("Ignored until we figure out why it is flaky b/336561027")
- @Test(expected = FileNotFoundException::class)
- fun read_fileNotFoundException() {
- underTest.readBytesFromDisk()
- }
-
@Test(expected = FileNotFoundException::class)
fun clear_returnsTrueWhenFileDeleted() {
// Write bytes to disk
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
index 6a0462b..c39c3fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt
@@ -16,7 +16,7 @@
package com.android.systemui.deviceentry.domain.ui.viewmodel
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository
@@ -24,7 +24,9 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.data.ui.viewmodel.deviceEntryUdfpsAccessibilityOverlayViewModel
+import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel
import com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
@@ -34,19 +36,22 @@
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition
import com.android.systemui.kosmos.testScope
-import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@ExperimentalCoroutinesApi
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class UdfpsAccessibilityOverlayViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(FULL_SCREEN_USER_SWITCHER, false) }
@@ -59,8 +64,27 @@
private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository
private val deviceEntryFingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository
private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
- private val shadeRepository = kosmos.fakeShadeRepository
- private val underTest = kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel
+
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
+
+ private lateinit var underTest: DeviceEntryUdfpsAccessibilityOverlayViewModel
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ @Before
+ fun setup() {
+ underTest = kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel
+ }
@Test
fun visible() =
@@ -142,7 +166,7 @@
)
// Shade not expanded
- shadeRepository.qsExpansion.value = 0f
- shadeRepository.lockscreenShadeExpansion.value = 0f
+ shadeTestUtil.setQsExpansion(0f)
+ shadeTestUtil.setLockscreenShadeExpansion(0f)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
index bcaad01..f5b5261 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
@@ -19,24 +19,20 @@
package com.android.systemui.keyguard.data.repository
-import android.os.fakeExecutorHandler
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
-import com.android.systemui.concurrency.fakeExecutor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
-import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
+import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.util.ThreadAssert
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -50,31 +46,32 @@
class KeyguardBlueprintRepositoryTest : SysuiTestCase() {
private lateinit var underTest: KeyguardBlueprintRepository
@Mock lateinit var configurationRepository: ConfigurationRepository
- @Mock lateinit var defaultLockscreenBlueprint: DefaultKeyguardBlueprint
@Mock lateinit var threadAssert: ThreadAssert
+
private val testScope = TestScope(StandardTestDispatcher())
private val kosmos: Kosmos = testKosmos()
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
- with(kosmos) {
- whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT)
- underTest =
- KeyguardBlueprintRepository(
- setOf(defaultLockscreenBlueprint),
- fakeExecutorHandler,
- threadAssert,
- )
- }
+ underTest = kosmos.keyguardBlueprintRepository
}
@Test
fun testApplyBlueprint_DefaultLayout() {
testScope.runTest {
val blueprint by collectLastValue(underTest.blueprint)
- underTest.applyBlueprint(defaultLockscreenBlueprint)
- assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint)
+ underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)
+ assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
+ }
+ }
+
+ @Test
+ fun testApplyBlueprint_SplitShadeLayout() {
+ testScope.runTest {
+ val blueprint by collectLastValue(underTest.blueprint)
+ underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)
+ assertThat(blueprint).isEqualTo(kosmos.splitShadeBlueprint)
}
}
@@ -83,33 +80,22 @@
testScope.runTest {
val blueprint by collectLastValue(underTest.blueprint)
underTest.refreshBlueprint()
- assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint)
+ assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
}
}
@Test
- fun testTransitionToLayout_validId() {
- assertThat(underTest.applyBlueprint(DEFAULT)).isTrue()
+ fun testTransitionToDefaultLayout_validId() {
+ assertThat(underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)).isTrue()
+ }
+
+ @Test
+ fun testTransitionToSplitShadeLayout_validId() {
+ assertThat(underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)).isTrue()
}
@Test
fun testTransitionToLayout_invalidId() {
assertThat(underTest.applyBlueprint("abc")).isFalse()
}
-
- @Test
- fun testTransitionToSameBlueprint_refreshesBlueprint() =
- with(kosmos) {
- testScope.runTest {
- val transition by collectLastValue(underTest.refreshTransition)
- fakeExecutor.runAllReady()
- runCurrent()
-
- underTest.applyBlueprint(defaultLockscreenBlueprint)
- fakeExecutor.runAllReady()
- runCurrent()
-
- assertThat(transition).isNotNull()
- }
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
index ac5823e..0bdf47a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
@@ -29,6 +29,7 @@
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
+import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository
import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
import com.android.systemui.kosmos.testScope
@@ -40,6 +41,7 @@
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -54,7 +56,7 @@
class KeyguardBlueprintInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val underTest by lazy { kosmos.keyguardBlueprintInteractor }
+ private val underTest = kosmos.keyguardBlueprintInteractor
private val clockRepository by lazy { kosmos.fakeKeyguardClockRepository }
private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
private val fingerprintPropertyRepository by lazy { kosmos.fakeFingerprintPropertyRepository }
@@ -79,8 +81,9 @@
val blueprintId by collectLastValue(underTest.blueprintId)
kosmos.shadeRepository.setShadeMode(ShadeMode.Single)
configurationRepository.onConfigurationChange()
- runCurrent()
+ runCurrent()
+ advanceUntilIdle()
assertThat(blueprintId).isEqualTo(DefaultKeyguardBlueprint.Companion.DEFAULT)
}
}
@@ -92,8 +95,9 @@
val blueprintId by collectLastValue(underTest.blueprintId)
kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
configurationRepository.onConfigurationChange()
- runCurrent()
+ runCurrent()
+ advanceUntilIdle()
assertThat(blueprintId).isEqualTo(SplitShadeKeyguardBlueprint.Companion.ID)
}
}
@@ -102,12 +106,13 @@
@DisableFlags(Flags.FLAG_COMPOSE_LOCKSCREEN)
fun fingerprintPropertyInitialized_updatesBlueprint() {
testScope.runTest {
- val blueprintId by collectLastValue(underTest.blueprintId)
- kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
- fingerprintPropertyRepository.supportsUdfps() // initialize properties
- runCurrent()
+ assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNull()
- assertThat(blueprintId).isEqualTo(SplitShadeKeyguardBlueprint.Companion.ID)
+ fingerprintPropertyRepository.supportsUdfps() // initialize properties
+
+ runCurrent()
+ advanceUntilIdle()
+ assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNotNull()
}
}
@@ -119,9 +124,23 @@
kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
clockRepository.setCurrentClock(clockController)
configurationRepository.onConfigurationChange()
- runCurrent()
+ runCurrent()
+ advanceUntilIdle()
assertThat(blueprintId).isEqualTo(DefaultKeyguardBlueprint.DEFAULT)
}
}
+
+ @Test
+ fun testRefreshFromConfigChange() {
+ testScope.runTest {
+ assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNull()
+
+ configurationRepository.onConfigurationChange()
+
+ runCurrent()
+ advanceUntilIdle()
+ assertThat(kosmos.keyguardBlueprintRepository.targetTransitionConfig).isNotNull()
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 1dc58d1..687e91a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import android.app.StatusBarManager
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.keyguard.KeyguardSecurityModel
@@ -29,7 +30,9 @@
import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.dock.fakeDockManager
+import com.android.systemui.flags.BrokenWithSceneContainer
import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.andSceneContainer
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.fakeCommandQueue
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -46,7 +49,8 @@
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.shade.data.repository.fakeShadeRepository
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.commandQueue
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
@@ -61,13 +65,14 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.reset
import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
/**
* Class for testing user journeys through the interactors. They will all be activated during setup,
@@ -75,8 +80,8 @@
*/
@ExperimentalCoroutinesApi
@SmallTest
-@RunWith(JUnit4::class)
-class KeyguardTransitionScenariosTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository())
@@ -87,7 +92,7 @@
private val keyguardRepository = kosmos.fakeKeyguardRepository
private val bouncerRepository = kosmos.fakeKeyguardBouncerRepository
private var commandQueue = kosmos.fakeCommandQueue
- private val shadeRepository = kosmos.fakeShadeRepository
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
private lateinit var featureFlags: FakeFeatureFlags
@@ -112,6 +117,18 @@
private val communalInteractor = kosmos.communalInteractor
private val dockManager = kosmos.fakeDockManager
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@@ -119,9 +136,11 @@
whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN)
mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
- mSetFlagsRule.disableFlags(
- Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR,
- )
+ if (!SceneContainerFlag.isEnabled) {
+ mSetFlagsRule.disableFlags(
+ Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR,
+ )
+ }
featureFlags = FakeFeatureFlags()
fromLockscreenTransitionInteractor.start()
@@ -210,6 +229,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun lockscreenToDreaming() =
testScope.runTest {
// GIVEN a device that is not dreaming or dozing
@@ -238,6 +258,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun lockscreenToDreamingLockscreenHosted() =
testScope.runTest {
// GIVEN a device that is not dreaming or dozing
@@ -527,6 +548,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun dozingToGoneWithUnlock() =
testScope.runTest {
// GIVEN a prior transition has run to DOZING
@@ -706,6 +728,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun goneToLockscreen() =
testScope.runTest {
// GIVEN a prior transition has run to GONE
@@ -755,6 +778,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun goneToGlanceableHub() =
testScope.runTest {
// GIVEN a prior transition has run to GONE
@@ -897,6 +921,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun alternateBouncerToGone() =
testScope.runTest {
// GIVEN a prior transition has run to ALTERNATE_BOUNCER
@@ -1135,6 +1160,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun occludedToGone() =
testScope.runTest {
// GIVEN a device on lockscreen
@@ -1165,6 +1191,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun occludedToLockscreen() =
testScope.runTest {
// GIVEN a device on lockscreen
@@ -1193,6 +1220,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun occludedToGlanceableHub() =
testScope.runTest {
// GIVEN a device on lockscreen
@@ -1229,6 +1257,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun occludedToGlanceableHubWhenInitiallyOnHub() =
testScope.runTest {
// GIVEN a device on lockscreen and communal is available
@@ -1314,6 +1343,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun primaryBouncerToOccluded() =
testScope.runTest {
// GIVEN a prior transition has run to PRIMARY_BOUNCER
@@ -1339,6 +1369,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun dozingToOccluded() =
testScope.runTest {
// GIVEN a prior transition has run to DOZING
@@ -1364,6 +1395,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun dreamingToOccluded() =
testScope.runTest {
// GIVEN a prior transition has run to DREAMING
@@ -1484,6 +1516,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun lockscreenToOccluded() =
testScope.runTest {
// GIVEN a prior transition has run to LOCKSCREEN
@@ -1507,6 +1540,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun aodToOccluded() =
testScope.runTest {
// GIVEN a prior transition has run to AOD
@@ -1553,6 +1587,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun lockscreenToOccluded_fromCameraGesture() =
testScope.runTest {
// GIVEN a prior transition has run to LOCKSCREEN
@@ -1586,6 +1621,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun lockscreenToPrimaryBouncerDragging() =
testScope.runTest {
// GIVEN a prior transition has run to LOCKSCREEN
@@ -1595,8 +1631,8 @@
// GIVEN the keyguard is showing locked
keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
runCurrent()
- shadeRepository.setLegacyShadeTracking(true)
- shadeRepository.setLegacyShadeExpansion(.9f)
+ shadeTestUtil.setTracking(true)
+ shadeTestUtil.setShadeExpansion(.9f)
runCurrent()
// THEN a transition from LOCKSCREEN => PRIMARY_BOUNCER should occur
@@ -1613,8 +1649,8 @@
// WHEN the user stops dragging and shade is back to expanded
clearInvocations(transitionRepository)
runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER)
- shadeRepository.setLegacyShadeTracking(false)
- shadeRepository.setLegacyShadeExpansion(1f)
+ shadeTestUtil.setTracking(false)
+ shadeTestUtil.setShadeExpansion(1f)
runCurrent()
// THEN a transition from LOCKSCREEN => PRIMARY_BOUNCER should occur
@@ -1803,6 +1839,7 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
fun glanceableHubToOccluded() =
testScope.runTest {
// GIVEN a prior transition has run to GLANCEABLE_HUB
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
index b1a8dd1..a77169e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt
@@ -18,20 +18,29 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
+import com.android.systemui.flags.DisableSceneContainer
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
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.scene.data.repository.sceneContainerRepository
+import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -57,14 +66,22 @@
.thenReturn(surfaceBehindIsAnimatingFlow)
}
- private val underTest = kosmos.windowManagerLockscreenVisibilityInteractor
+ private val underTest = lazy { kosmos.windowManagerLockscreenVisibilityInteractor }
private val testScope = kosmos.testScope
private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+ @Before
+ fun setUp() {
+ // lazy value needs to be called here otherwise flow collection misbehaves
+ underTest.value
+ kosmos.sceneContainerRepository.setTransitionState(sceneTransitions)
+ }
+
@Test
+ @DisableSceneContainer
fun surfaceBehindVisibility_switchesToCorrectFlow() =
testScope.runTest {
- val values by collectValues(underTest.surfaceBehindVisibility)
+ val values by collectValues(underTest.value.surfaceBehindVisibility)
// Start on LOCKSCREEN.
transitionRepository.sendTransitionStep(
@@ -170,9 +187,10 @@
}
@Test
+ @DisableSceneContainer
fun testUsingGoingAwayAnimation_duringTransitionToGone() =
testScope.runTest {
- val values by collectValues(underTest.usingKeyguardGoingAwayAnimation)
+ val values by collectValues(underTest.value.usingKeyguardGoingAwayAnimation)
// Start on LOCKSCREEN.
transitionRepository.sendTransitionStep(
@@ -230,9 +248,10 @@
}
@Test
+ @DisableSceneContainer
fun testNotUsingGoingAwayAnimation_evenWhenAnimating_ifStateIsNotGone() =
testScope.runTest {
- val values by collectValues(underTest.usingKeyguardGoingAwayAnimation)
+ val values by collectValues(underTest.value.usingKeyguardGoingAwayAnimation)
// Start on LOCKSCREEN.
transitionRepository.sendTransitionStep(
@@ -319,9 +338,10 @@
}
@Test
+ @DisableSceneContainer
fun lockscreenVisibility_visibleWhenGone() =
testScope.runTest {
- val values by collectValues(underTest.lockscreenVisibility)
+ val values by collectValues(underTest.value.lockscreenVisibility)
// Start on LOCKSCREEN.
transitionRepository.sendTransitionStep(
@@ -385,9 +405,10 @@
}
@Test
+ @DisableSceneContainer
fun testLockscreenVisibility_usesFromState_ifCanceled() =
testScope.runTest {
- val values by collectValues(underTest.lockscreenVisibility)
+ val values by collectValues(underTest.value.lockscreenVisibility)
transitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
@@ -486,9 +507,10 @@
* state during the AOD/isAsleep -> GONE transition is AOD (where lockscreen visibility = true).
*/
@Test
+ @DisableSceneContainer
fun testLockscreenVisibility_falseDuringTransitionToGone_fromCanceledGone() =
testScope.runTest {
- val values by collectValues(underTest.lockscreenVisibility)
+ val values by collectValues(underTest.value.lockscreenVisibility)
transitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
@@ -587,11 +609,11 @@
)
}
- /** */
@Test
+ @DisableSceneContainer
fun testLockscreenVisibility_trueDuringTransitionToGone_fromNotCanceledGone() =
testScope.runTest {
- val values by collectValues(underTest.lockscreenVisibility)
+ val values by collectValues(underTest.value.lockscreenVisibility)
transitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
@@ -702,4 +724,109 @@
values
)
}
+
+ @Test
+ @EnableSceneContainer
+ fun sceneContainer_lockscreenVisibility_visibleWhenNotGone() =
+ testScope.runTest {
+ val lockscreenVisibility by collectLastValue(underTest.value.lockscreenVisibility)
+
+ sceneTransitions.value = lsToGone
+ assertThat(lockscreenVisibility).isTrue()
+
+ sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
+ assertThat(lockscreenVisibility).isFalse()
+
+ sceneTransitions.value = goneToLs
+ assertThat(lockscreenVisibility).isFalse()
+
+ sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+ assertThat(lockscreenVisibility).isTrue()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun sceneContainer_lockscreenVisibility_notVisibleWhenReturningToGone() =
+ testScope.runTest {
+ val lockscreenVisibility by collectLastValue(underTest.value.lockscreenVisibility)
+
+ sceneTransitions.value = goneToLs
+ assertThat(lockscreenVisibility).isFalse()
+
+ sceneTransitions.value = lsToGone
+ assertThat(lockscreenVisibility).isFalse()
+
+ sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
+ assertThat(lockscreenVisibility).isFalse()
+
+ sceneTransitions.value = goneToLs
+ assertThat(lockscreenVisibility).isFalse()
+
+ sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+ assertThat(lockscreenVisibility).isTrue()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun sceneContainer_usingGoingAwayAnimation_duringTransitionToGone() =
+ testScope.runTest {
+ val usingKeyguardGoingAwayAnimation by
+ collectLastValue(underTest.value.usingKeyguardGoingAwayAnimation)
+
+ sceneTransitions.value = lsToGone
+ assertThat(usingKeyguardGoingAwayAnimation).isTrue()
+
+ sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
+ assertThat(usingKeyguardGoingAwayAnimation).isFalse()
+ }
+
+ @Test
+ @EnableSceneContainer
+ fun sceneContainer_usingGoingAwayAnimation_surfaceBehindIsAnimating() =
+ testScope.runTest {
+ val usingKeyguardGoingAwayAnimation by
+ collectLastValue(underTest.value.usingKeyguardGoingAwayAnimation)
+
+ sceneTransitions.value = lsToGone
+ surfaceBehindIsAnimatingFlow.emit(true)
+ assertThat(usingKeyguardGoingAwayAnimation).isTrue()
+
+ sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Gone)
+ assertThat(usingKeyguardGoingAwayAnimation).isTrue()
+
+ sceneTransitions.value = goneToLs
+ assertThat(usingKeyguardGoingAwayAnimation).isTrue()
+
+ surfaceBehindIsAnimatingFlow.emit(false)
+ assertThat(usingKeyguardGoingAwayAnimation).isFalse()
+ }
+
+ companion object {
+ private val progress = MutableStateFlow(0f)
+
+ private val sceneTransitions =
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(Scenes.Lockscreen)
+ )
+
+ private val lsToGone =
+ ObservableTransitionState.Transition(
+ Scenes.Lockscreen,
+ Scenes.Gone,
+ flowOf(Scenes.Lockscreen),
+ progress,
+ false,
+ flowOf(false)
+ )
+
+ private val goneToLs =
+ ObservableTransitionState.Transition(
+ Scenes.Gone,
+ Scenes.Lockscreen,
+ flowOf(Scenes.Lockscreen),
+ progress,
+ false,
+ flowOf(false)
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
index dbf6a29..8a0613f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt
@@ -66,25 +66,19 @@
fun testHelp() {
command().execute(pw, listOf("help"))
verify(pw, atLeastOnce()).println(anyString())
- verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString())
+ verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString())
}
@Test
fun testBlank() {
command().execute(pw, listOf())
verify(pw, atLeastOnce()).println(anyString())
- verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString())
+ verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString())
}
@Test
fun testValidArg() {
command().execute(pw, listOf("fake"))
- verify(keyguardBlueprintInteractor).transitionToBlueprint("fake")
- }
-
- @Test
- fun testValidArg_Int() {
- command().execute(pw, listOf("1"))
- verify(keyguardBlueprintInteractor).transitionToBlueprint(1)
+ verify(keyguardBlueprintInteractor).transitionOrRefreshBlueprint("fake")
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
index 0bca367..f61ddeb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelTest.kt
@@ -19,6 +19,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.fakeAccessibilityRepository
import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
import com.android.systemui.coroutines.collectLastValue
@@ -150,6 +151,51 @@
assertThat(iconType).isEqualTo(DeviceEntryIconView.IconType.NONE)
}
+ fun accessibilityDelegateHint_accessibilityNotEnabled() =
+ testScope.runTest {
+ val accessibilityDelegateHint by collectLastValue(underTest.accessibilityDelegateHint)
+ kosmos.fakeAccessibilityRepository.isEnabled.value = false
+ assertThat(accessibilityDelegateHint)
+ .isEqualTo(DeviceEntryIconView.AccessibilityHintType.NONE)
+ }
+
+ @Test
+ fun accessibilityDelegateHint_accessibilityEnabled_locked() =
+ testScope.runTest {
+ val accessibilityDelegateHint by collectLastValue(underTest.accessibilityDelegateHint)
+ kosmos.fakeAccessibilityRepository.isEnabled.value = true
+
+ // interactive lock icon
+ keyguardRepository.setKeyguardDismissible(false)
+ fingerprintPropertyRepository.supportsUdfps()
+
+ assertThat(accessibilityDelegateHint)
+ .isEqualTo(DeviceEntryIconView.AccessibilityHintType.AUTHENTICATE)
+
+ // non-interactive lock icon
+ keyguardRepository.setKeyguardDismissible(false)
+ fingerprintPropertyRepository.supportsRearFps()
+
+ assertThat(accessibilityDelegateHint)
+ .isEqualTo(DeviceEntryIconView.AccessibilityHintType.NONE)
+ }
+
+ @Test
+ fun accessibilityDelegateHint_accessibilityEnabled_unlocked() =
+ testScope.runTest {
+ val accessibilityDelegateHint by collectLastValue(underTest.accessibilityDelegateHint)
+ kosmos.fakeAccessibilityRepository.isEnabled.value = true
+
+ // interactive unlock icon
+ keyguardRepository.setKeyguardDismissible(true)
+ fingerprintPropertyRepository.supportsUdfps()
+ advanceTimeBy(UNLOCKED_DELAY_MS * 2) // wait for unlocked delay
+ runCurrent()
+
+ assertThat(accessibilityDelegateHint)
+ .isEqualTo(DeviceEntryIconView.AccessibilityHintType.ENTER)
+ }
+
private fun deviceEntryIconTransitionAlpha(alpha: Float) {
deviceEntryIconTransition.setDeviceEntryParentViewAlpha(alpha)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 1881a9e..16421a0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -88,7 +88,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class KeyguardBottomAreaViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
@Mock private lateinit var expandable: Expandable
@Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
@@ -115,7 +115,7 @@
private val kosmos = testKosmos()
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
index 0c98cff..768d446 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
@@ -53,7 +53,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardClockViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class KeyguardClockViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
val kosmos = testKosmos()
val testScope = kosmos.testScope
val underTest = kosmos.keyguardClockViewModel
@@ -67,7 +67,7 @@
var faceConfig = ClockFaceConfig()
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt
deleted file mode 100644
index 8cc3a85..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt
+++ /dev/null
@@ -1,61 +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.panels.domain.repository
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.qs.panels.data.repository.IconTilesRepositoryImpl
-import com.android.systemui.qs.pipeline.shared.TileSpec
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-class IconTilesRepositoryImplTest : SysuiTestCase() {
-
- private val underTest = IconTilesRepositoryImpl()
-
- @Test
- fun iconTilesSpecsIsValid() = runTest {
- val tilesSpecs by collectLastValue(underTest.iconTilesSpecs)
- assertThat(tilesSpecs).isEqualTo(ICON_ONLY_TILES_SPECS)
- }
-
- companion object {
- private val ICON_ONLY_TILES_SPECS =
- setOf(
- TileSpec.create("airplane"),
- TileSpec.create("battery"),
- TileSpec.create("cameratoggle"),
- TileSpec.create("cast"),
- TileSpec.create("color_correction"),
- TileSpec.create("inversion"),
- TileSpec.create("saver"),
- TileSpec.create("dnd"),
- TileSpec.create("flashlight"),
- TileSpec.create("location"),
- TileSpec.create("mictoggle"),
- TileSpec.create("nfc"),
- TileSpec.create("night"),
- TileSpec.create("rotation")
- )
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
index 2536a93..9798562 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java
@@ -5,6 +5,7 @@
import static android.telephony.SignalStrength.NUM_SIGNAL_STRENGTH_BINS;
import static android.telephony.SignalStrength.SIGNAL_STRENGTH_GREAT;
import static android.telephony.SignalStrength.SIGNAL_STRENGTH_POOR;
+import static android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.settingslib.wifi.WifiUtils.getHotspotIconResource;
@@ -217,6 +218,8 @@
when(mAccessPointController.getMergedCarrierEntry()).thenReturn(mMergedCarrierEntry);
when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{SUB_ID});
when(SubscriptionManager.getDefaultDataSubscriptionId()).thenReturn(SUB_ID);
+ SubscriptionInfo info = mock(SubscriptionInfo.class);
+ when(mSubscriptionManager.getActiveSubscriptionInfo(SUB_ID)).thenReturn(info);
when(mToastFactory.createToast(any(), anyString(), anyString(), anyInt(), anyInt()))
.thenReturn(mSystemUIToast);
when(mSystemUIToast.getView()).thenReturn(mToastView);
@@ -1083,19 +1086,34 @@
}
@Test
- public void hasActiveSubId_activeSubIdListIsEmpty_returnFalse() {
- when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{});
+ public void hasActiveSubIdOnDds_noDds_returnFalse() {
+ when(SubscriptionManager.getDefaultDataSubscriptionId())
+ .thenReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
+
mInternetDialogController.mOnSubscriptionsChangedListener.onSubscriptionsChanged();
- assertThat(mInternetDialogController.hasActiveSubId()).isFalse();
+ assertThat(mInternetDialogController.hasActiveSubIdOnDds()).isFalse();
}
@Test
- public void hasActiveSubId_activeSubIdListNotEmpty_returnTrue() {
- when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{SUB_ID});
+ public void hasActiveSubIdOnDds_activeDds_returnTrue() {
mInternetDialogController.mOnSubscriptionsChangedListener.onSubscriptionsChanged();
- assertThat(mInternetDialogController.hasActiveSubId()).isTrue();
+ assertThat(mInternetDialogController.hasActiveSubIdOnDds()).isTrue();
+ }
+
+ @Test
+ public void hasActiveSubIdOnDds_activeDdsAndHasProvisioning_returnFalse() {
+ when(SubscriptionManager.getDefaultDataSubscriptionId())
+ .thenReturn(SUB_ID);
+ SubscriptionInfo info = mock(SubscriptionInfo.class);
+ when(info.isEmbedded()).thenReturn(true);
+ when(info.getProfileClass()).thenReturn(PROFILE_CLASS_PROVISIONING);
+ when(mSubscriptionManager.getActiveSubscriptionInfo(SUB_ID)).thenReturn(info);
+
+ mInternetDialogController.mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+ assertThat(mInternetDialogController.hasActiveSubIdOnDds()).isFalse();
}
private String getResourcesString(String name) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java
index 6f88891..aefcc87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java
@@ -251,7 +251,7 @@
// Mobile network should be gone if the list of active subscriptionId is null.
when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(false);
when(mInternetDialogController.isAirplaneModeEnabled()).thenReturn(false);
- when(mInternetDialogController.hasActiveSubId()).thenReturn(false);
+ when(mInternetDialogController.hasActiveSubIdOnDds()).thenReturn(false);
mInternetDialogDelegate.updateDialog(true);
@@ -336,7 +336,7 @@
@Test
public void updateDialog_mobileDataIsEnabled_checkMobileDataSwitch() {
- doReturn(true).when(mInternetDialogController).hasActiveSubId();
+ doReturn(true).when(mInternetDialogController).hasActiveSubIdOnDds();
when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(true);
when(mInternetDialogController.isMobileDataEnabled()).thenReturn(true);
mMobileToggleSwitch.setChecked(false);
@@ -348,7 +348,7 @@
@Test
public void updateDialog_mobileDataIsNotChanged_checkMobileDataSwitch() {
- doReturn(true).when(mInternetDialogController).hasActiveSubId();
+ doReturn(true).when(mInternetDialogController).hasActiveSubIdOnDds();
when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(true);
when(mInternetDialogController.isMobileDataEnabled()).thenReturn(false);
mMobileToggleSwitch.setChecked(false);
@@ -361,7 +361,7 @@
@Test
public void updateDialog_wifiOnAndHasInternetWifi_showConnectedWifi() {
mInternetDialogDelegate.dismissDialog();
- doReturn(true).when(mInternetDialogController).hasActiveSubId();
+ doReturn(true).when(mInternetDialogController).hasActiveSubIdOnDds();
createInternetDialog();
// The preconditions WiFi ON and Internet WiFi are already in setUp()
doReturn(false).when(mInternetDialogController).activeNetworkIsCellular();
@@ -522,7 +522,7 @@
public void updateDialog_showSecondaryDataSub() {
mInternetDialogDelegate.dismissDialog();
doReturn(1).when(mInternetDialogController).getActiveAutoSwitchNonDdsSubId();
- doReturn(true).when(mInternetDialogController).hasActiveSubId();
+ doReturn(true).when(mInternetDialogController).hasActiveSubIdOnDds();
doReturn(false).when(mInternetDialogController).isAirplaneModeEnabled();
createInternetDialog();
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 3793970..5b47c94 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -446,6 +446,7 @@
mUiEventLogger,
() -> mKosmos.getInteractionJankMonitor(),
mJavaAdapter,
+ () -> mKeyguardTransitionInteractor,
() -> mShadeInteractor,
() -> mKosmos.getDeviceUnlockedInteractor(),
() -> mKosmos.getSceneInteractor(),
@@ -600,6 +601,7 @@
new UiEventLoggerFake(),
() -> mKosmos.getInteractionJankMonitor(),
mJavaAdapter,
+ () -> mKeyguardTransitionInteractor,
() -> mShadeInteractor,
() -> mKosmos.getDeviceUnlockedInteractor(),
() -> mKosmos.getSceneInteractor(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index a867b0f..45d0102 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -102,7 +102,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
@RunWithLooper(setAsMainLooper = true)
-class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : SysuiTestCase() {
@Mock private lateinit var view: NotificationShadeWindowView
@Mock private lateinit var sysuiStatusBarStateController: SysuiStatusBarStateController
@@ -160,7 +160,7 @@
private lateinit var featureFlagsClassic: FakeFeatureFlagsClassic
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 347620a..83ad18b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -55,7 +55,7 @@
@RunWith(ParameterizedAndroidJunit4::class)
@SmallTest
@EnableFlags(FooterViewRefactor.FLAG_NAME)
-class FooterViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class FooterViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
@@ -79,7 +79,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProviderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProviderImplTest.kt
new file mode 100644
index 0000000..5e50af3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/HeadsUpStyleProviderImplTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Flags.FLAG_COMPACT_HEADS_UP_NOTIFICATION
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class HeadsUpStyleProviderImplTest : SysuiTestCase() {
+
+ @Rule @JvmField val setFlagsRule = SetFlagsRule()
+
+ private lateinit var statusBarModeRepositoryStore: FakeStatusBarModeRepository
+ private lateinit var headsUpStyleProvider: HeadsUpStyleProviderImpl
+
+ @Before
+ fun setUp() {
+ statusBarModeRepositoryStore = FakeStatusBarModeRepository()
+ statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value = true
+
+ headsUpStyleProvider = HeadsUpStyleProviderImpl(statusBarModeRepositoryStore)
+ }
+
+ @Test
+ @DisableFlags(FLAG_COMPACT_HEADS_UP_NOTIFICATION)
+ fun shouldApplyCompactStyle_returnsFalse_whenCompactFlagDisabled() {
+ assertThat(headsUpStyleProvider.shouldApplyCompactStyle()).isFalse()
+ }
+
+ @Test
+ @EnableFlags(FLAG_COMPACT_HEADS_UP_NOTIFICATION)
+ fun shouldApplyCompactStyle_returnsTrue_whenImmersiveModeEnabled() {
+ // GIVEN
+ statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value = true
+
+ // THEN
+ assertThat(headsUpStyleProvider.shouldApplyCompactStyle()).isTrue()
+ }
+
+ @Test
+ @EnableFlags(FLAG_COMPACT_HEADS_UP_NOTIFICATION)
+ fun shouldApplyCompactStyle_returnsFalse_whenImmersiveModeDisabled() {
+ // GIVEN
+ statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode.value = false
+
+ // THEN
+ assertThat(headsUpStyleProvider.shouldApplyCompactStyle()).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
index 9b4f931..cb40f72 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt
@@ -66,7 +66,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
@TestableLooper.RunWithLooper
-class KeyguardBypassControllerTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class KeyguardBypassControllerTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val featureFlags = FakeFeatureFlags()
@@ -92,7 +92,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Captor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index 127a3d7..269510e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -168,6 +168,8 @@
private FakePowerRepository mPowerRepository;
@Mock
private UserTracker mUserTracker;
+ @Mock
+ private HeadsUpManager mHeadsUpManager;
private final FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
private ExpandableNotificationRow mNotificationRow;
private ExpandableNotificationRow mBubbleNotificationRow;
@@ -222,13 +224,12 @@
mScreenOffAnimationController,
mStatusBarStateController).getPowerInteractor();
- HeadsUpManager headsUpManager = mock(HeadsUpManager.class);
NotificationLaunchAnimatorControllerProvider notificationAnimationProvider =
new NotificationLaunchAnimatorControllerProvider(
new NotificationLaunchAnimationInteractor(
new NotificationLaunchAnimationRepository()),
mock(NotificationListContainer.class),
- headsUpManager,
+ mHeadsUpManager,
mJankMonitor);
mNotificationActivityStarter =
new StatusBarNotificationActivityStarter(
@@ -237,7 +238,7 @@
mHandler,
mUiBgExecutor,
mVisibilityProvider,
- headsUpManager,
+ mHeadsUpManager,
mActivityStarter,
mCommandQueue,
mClickNotifier,
@@ -417,6 +418,51 @@
}
@Test
+ public void testOnNotificationBubbleIconClicked_unbubble_keyGuardShowing()
+ throws RemoteException {
+ NotificationEntry entry = mBubbleNotificationRow.getEntry();
+ StatusBarNotification sbn = entry.getSbn();
+
+ // Given
+ sbn.getNotification().contentIntent = mContentIntent;
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
+
+ // When
+ mNotificationActivityStarter.onNotificationBubbleIconClicked(entry);
+
+ // Then
+ verify(mBubblesManager).onUserChangedBubble(entry, false);
+
+ verify(mHeadsUpManager).removeNotification(entry.getKey(), true);
+
+ verifyNoMoreInteractions(mContentIntent);
+ verifyNoMoreInteractions(mShadeController);
+ }
+
+ @Test
+ public void testOnNotificationBubbleIconClicked_bubble_keyGuardShowing() {
+ NotificationEntry entry = mNotificationRow.getEntry();
+ StatusBarNotification sbn = entry.getSbn();
+
+ // Given
+ sbn.getNotification().contentIntent = mContentIntent;
+ when(mKeyguardStateController.isShowing()).thenReturn(true);
+ when(mKeyguardStateController.isOccluded()).thenReturn(true);
+
+ // When
+ mNotificationActivityStarter.onNotificationBubbleIconClicked(entry);
+
+ // Then
+ verify(mBubblesManager).onUserChangedBubble(entry, true);
+
+ verify(mHeadsUpManager).removeNotification(entry.getKey(), true);
+
+ verify(mContentIntent, atLeastOnce()).isActivity();
+ verifyNoMoreInteractions(mContentIntent);
+ }
+
+ @Test
public void testOnFullScreenIntentWhenDozing_wakeUpDevice() {
// GIVEN entry that can has a full screen intent that can show
PendingIntent fullScreenIntent = PendingIntent.getActivity(mContext, 1,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index b5525b1..36df61d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -295,6 +295,50 @@
}
@Test
+ fun subscriptions_subIsOnlyNtn_modelHasExclusivelyNtnTrue() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.subscriptions)
+
+ val onlyNtnSub =
+ mock<SubscriptionInfo>().also {
+ whenever(it.isOnlyNonTerrestrialNetwork).thenReturn(true)
+ whenever(it.subscriptionId).thenReturn(45)
+ whenever(it.groupUuid).thenReturn(GROUP_1)
+ whenever(it.carrierName).thenReturn("NTN only")
+ whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+ }
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(onlyNtnSub))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].isExclusivelyNonTerrestrial).isTrue()
+ }
+
+ @Test
+ fun subscriptions_subIsNotOnlyNtn_modelHasExclusivelyNtnFalse() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.subscriptions)
+
+ val notOnlyNtnSub =
+ mock<SubscriptionInfo>().also {
+ whenever(it.isOnlyNonTerrestrialNetwork).thenReturn(false)
+ whenever(it.subscriptionId).thenReturn(45)
+ whenever(it.groupUuid).thenReturn(GROUP_1)
+ whenever(it.carrierName).thenReturn("NTN only")
+ whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+ }
+
+ whenever(subscriptionManager.completeActiveSubscriptionInfoList)
+ .thenReturn(listOf(notOnlyNtnSub))
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].isExclusivelyNonTerrestrial).isFalse()
+ }
+
+ @Test
fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() =
testScope.runTest {
val latest by collectLastValue(underTest.subscriptions)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 0b14be1..0f9cbfa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -42,14 +42,11 @@
import com.google.common.truth.Truth.assertThat
import java.util.UUID
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.yield
-import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
@@ -68,7 +65,7 @@
set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true)
}
- private val testDispatcher = UnconfinedTestDispatcher()
+ private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
private val tableLogBuffer =
@@ -113,17 +110,12 @@
)
}
- @After fun tearDown() {}
-
@Test
fun filteredSubscriptions_default() =
testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf<SubscriptionModel>())
-
- job.cancel()
}
// Based on the logic from the old pipeline, we'll never filter subs when there are more than 2
@@ -133,12 +125,9 @@
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(SUB_1, SUB_3_OPP, SUB_4_OPP))
-
- job.cancel()
}
@Test
@@ -146,12 +135,9 @@
testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2))
-
- job.cancel()
}
@Test
@@ -160,12 +146,9 @@
connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(SUB_3_OPP, SUB_4_OPP))
-
- job.cancel()
}
@Test
@@ -180,12 +163,9 @@
connectionsRepository.setSubscriptions(listOf(sub1, sub2))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(sub1, sub2))
-
- job.cancel()
}
@Test
@@ -202,13 +182,10 @@
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(false)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
// Filtered subscriptions should show the active one when the config is false
assertThat(latest).isEqualTo(listOf(sub3))
-
- job.cancel()
}
@Test
@@ -225,13 +202,10 @@
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(false)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
// Filtered subscriptions should show the active one when the config is false
assertThat(latest).isEqualTo(listOf(sub4))
-
- job.cancel()
}
@Test
@@ -248,14 +222,11 @@
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(true)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
// Filtered subscriptions should show the primary (non-opportunistic) if the config is
// true
assertThat(latest).isEqualTo(listOf(sub1))
-
- job.cancel()
}
@Test
@@ -272,14 +243,11 @@
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(true)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
// Filtered subscriptions should show the primary (non-opportunistic) if the config is
// true
assertThat(latest).isEqualTo(listOf(sub1))
-
- job.cancel()
}
@Test
@@ -297,12 +265,9 @@
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(false)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(sub3))
-
- job.cancel()
}
@Test
@@ -320,12 +285,9 @@
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
.thenReturn(false)
- var latest: List<SubscriptionModel>? = null
- val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.filteredSubscriptions)
assertThat(latest).isEqualTo(listOf(sub1))
-
- job.cancel()
}
@Test
@@ -446,313 +408,345 @@
}
@Test
+ fun filteredSubscriptions_subNotExclusivelyNonTerrestrial_hasSub() =
+ testScope.runTest {
+ val notExclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+
+ connectionsRepository.setSubscriptions(listOf(notExclusivelyNonTerrestrialSub))
+
+ val latest by collectLastValue(underTest.filteredSubscriptions)
+
+ assertThat(latest).isEqualTo(listOf(notExclusivelyNonTerrestrialSub))
+ }
+
+ @Test
+ fun filteredSubscriptions_subExclusivelyNonTerrestrial_doesNotHaveSub() =
+ testScope.runTest {
+ val exclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = true,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+
+ connectionsRepository.setSubscriptions(listOf(exclusivelyNonTerrestrialSub))
+
+ val latest by collectLastValue(underTest.filteredSubscriptions)
+
+ assertThat(latest).isEmpty()
+ }
+
+ @Test
+ fun filteredSubscription_mixOfExclusivelyNonTerrestrialAndOther_hasOtherSubsOnly() =
+ testScope.runTest {
+ val exclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = true,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ val otherSub1 =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 1,
+ carrierName = "Carrier 1",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+ val otherSub2 =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = false,
+ subscriptionId = 2,
+ carrierName = "Carrier 2",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+
+ connectionsRepository.setSubscriptions(
+ listOf(otherSub1, exclusivelyNonTerrestrialSub, otherSub2)
+ )
+
+ val latest by collectLastValue(underTest.filteredSubscriptions)
+
+ assertThat(latest).isEqualTo(listOf(otherSub1, otherSub2))
+ }
+
+ @Test
+ fun filteredSubscriptions_exclusivelyNonTerrestrialSub_andOpportunistic_bothFiltersHappen() =
+ testScope.runTest {
+ // Exclusively non-terrestrial sub
+ val exclusivelyNonTerrestrialSub =
+ SubscriptionModel(
+ isExclusivelyNonTerrestrial = true,
+ subscriptionId = 5,
+ carrierName = "Carrier 5",
+ profileClass = PROFILE_CLASS_UNSET,
+ )
+
+ // Opportunistic subs
+ val (sub3, sub4) =
+ createSubscriptionPair(
+ subscriptionIds = Pair(SUB_3_ID, SUB_4_ID),
+ opportunistic = Pair(true, true),
+ grouped = true,
+ )
+
+ // WHEN both an exclusively non-terrestrial sub and opportunistic sub pair is included
+ connectionsRepository.setSubscriptions(listOf(sub3, sub4, exclusivelyNonTerrestrialSub))
+ connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
+
+ val latest by collectLastValue(underTest.filteredSubscriptions)
+
+ // THEN both the only-non-terrestrial sub and the non-active sub are filtered out,
+ // leaving only sub3.
+ assertThat(latest).isEqualTo(listOf(sub3))
+ }
+
+ @Test
fun activeDataConnection_turnedOn() =
testScope.runTest {
CONNECTION_1.setDataEnabled(true)
- var latest: Boolean? = null
- val job =
- underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+
+ val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun activeDataConnection_turnedOff() =
testScope.runTest {
CONNECTION_1.setDataEnabled(true)
- var latest: Boolean? = null
- val job =
- underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
CONNECTION_1.setDataEnabled(false)
- yield()
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun activeDataConnection_invalidSubId() =
testScope.runTest {
- var latest: Boolean? = null
- val job =
- underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.activeDataConnectionHasDataEnabled)
connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID)
- yield()
// An invalid active subId should tell us that data is off
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun failedConnection_default_validated_notFailed() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
- yield()
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun failedConnection_notDefault_notValidated_notFailed() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = false
connectionsRepository.defaultConnectionIsValidated.value = false
- yield()
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun failedConnection_default_notValidated_failed() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = false
- yield()
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun failedConnection_carrierMergedDefault_notValidated_failed() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.hasCarrierMergedConnection.value = true
connectionsRepository.defaultConnectionIsValidated.value = false
- yield()
assertThat(latest).isTrue()
-
- job.cancel()
}
/** Regression test for b/275076959. */
@Test
fun failedConnection_dataSwitchInSameGroup_notFailed() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
+ runCurrent()
// WHEN there's a data change in the same subscription group
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
connectionsRepository.defaultConnectionIsValidated.value = false
+ runCurrent()
// THEN the default connection is *not* marked as failed because of forced validation
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun failedConnection_dataSwitchNotInSameGroup_isFailed() =
testScope.runTest {
- var latestConnectionFailed: Boolean? = null
- val job =
- underTest.isDefaultConnectionFailed
- .onEach { latestConnectionFailed = it }
- .launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
+
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
+ runCurrent()
// WHEN the connection is invalidated without a activeSubChangedInGroupEvent
connectionsRepository.defaultConnectionIsValidated.value = false
// THEN the connection is immediately marked as failed
- assertThat(latestConnectionFailed).isTrue()
-
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun alwaysShowDataRatIcon_configHasTrue() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
val config = MobileMappings.Config()
config.alwaysShowDataRatIcon = true
connectionsRepository.defaultDataSubRatConfig.value = config
- yield()
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun alwaysShowDataRatIcon_configHasFalse() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.alwaysShowDataRatIcon)
val config = MobileMappings.Config()
config.alwaysShowDataRatIcon = false
connectionsRepository.defaultDataSubRatConfig.value = config
- yield()
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun alwaysUseCdmaLevel_configHasTrue() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
val config = MobileMappings.Config()
config.alwaysShowCdmaRssi = true
connectionsRepository.defaultDataSubRatConfig.value = config
- yield()
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun alwaysUseCdmaLevel_configHasFalse() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.alwaysUseCdmaLevel)
val config = MobileMappings.Config()
config.alwaysShowCdmaRssi = false
connectionsRepository.defaultDataSubRatConfig.value = config
- yield()
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun isSingleCarrier_zeroSubscriptions_false() =
testScope.runTest {
- var latest: Boolean? = true
- val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isSingleCarrier)
connectionsRepository.setSubscriptions(emptyList())
- assertThat(latest).isFalse()
- job.cancel()
+ assertThat(latest).isFalse()
}
@Test
fun isSingleCarrier_oneSubscription_true() =
testScope.runTest {
- var latest: Boolean? = false
- val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isSingleCarrier)
connectionsRepository.setSubscriptions(listOf(SUB_1))
- assertThat(latest).isTrue()
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun isSingleCarrier_twoSubscriptions_false() =
testScope.runTest {
- var latest: Boolean? = true
- val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isSingleCarrier)
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
- assertThat(latest).isFalse()
- job.cancel()
+ assertThat(latest).isFalse()
}
@Test
fun isSingleCarrier_updates() =
testScope.runTest {
- var latest: Boolean? = false
- val job = underTest.isSingleCarrier.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isSingleCarrier)
connectionsRepository.setSubscriptions(listOf(SUB_1))
assertThat(latest).isTrue()
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun mobileIsDefault_mobileFalseAndCarrierMergedFalse_false() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.mobileIsDefault)
connectionsRepository.mobileIsDefault.value = false
connectionsRepository.hasCarrierMergedConnection.value = false
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
fun mobileIsDefault_mobileTrueAndCarrierMergedFalse_true() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.mobileIsDefault)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.hasCarrierMergedConnection.value = false
assertThat(latest).isTrue()
-
- job.cancel()
}
/** Regression test for b/272586234. */
@Test
fun mobileIsDefault_mobileFalseAndCarrierMergedTrue_true() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.mobileIsDefault)
connectionsRepository.mobileIsDefault.value = false
connectionsRepository.hasCarrierMergedConnection.value = true
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun mobileIsDefault_updatesWhenRepoUpdates() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.mobileIsDefault.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.mobileIsDefault)
connectionsRepository.mobileIsDefault.value = true
assertThat(latest).isTrue()
@@ -762,8 +756,6 @@
connectionsRepository.hasCarrierMergedConnection.value = true
assertThat(latest).isTrue()
-
- job.cancel()
}
// The data switch tests are mostly testing the [forcingCellularValidation] flow, but that flow
@@ -772,95 +764,79 @@
@Test
fun dataSwitch_inSameGroup_validatedMatchesPreviousValue_expiresAfter2s() =
testScope.runTest {
- var latestConnectionFailed: Boolean? = null
- val job =
- underTest.isDefaultConnectionFailed
- .onEach { latestConnectionFailed = it }
- .launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
+ runCurrent()
// Trigger a data change in the same subscription group that's not yet validated
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
connectionsRepository.defaultConnectionIsValidated.value = false
+ runCurrent()
// After 1s, the force validation bit is still present, so the connection is not marked
// as failed
advanceTimeBy(1000)
- assertThat(latestConnectionFailed).isFalse()
+ assertThat(latest).isFalse()
// After 2s, the force validation expires so the connection updates to failed
advanceTimeBy(1001)
- assertThat(latestConnectionFailed).isTrue()
-
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun dataSwitch_inSameGroup_notValidated_immediatelyMarkedAsFailed() =
testScope.runTest {
- var latestConnectionFailed: Boolean? = null
- val job =
- underTest.isDefaultConnectionFailed
- .onEach { latestConnectionFailed = it }
- .launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = false
+ runCurrent()
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
- assertThat(latestConnectionFailed).isTrue()
-
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun dataSwitch_loseValidation_thenSwitchHappens_clearsForcedBit() =
testScope.runTest {
- var latestConnectionFailed: Boolean? = null
- val job =
- underTest.isDefaultConnectionFailed
- .onEach { latestConnectionFailed = it }
- .launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
// GIVEN the network starts validated
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
+ runCurrent()
// WHEN a data change happens in the same group
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
// WHEN the validation bit is lost
connectionsRepository.defaultConnectionIsValidated.value = false
+ runCurrent()
// WHEN another data change happens in the same group
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
// THEN the forced validation bit is still used...
- assertThat(latestConnectionFailed).isFalse()
+ assertThat(latest).isFalse()
advanceTimeBy(1000)
- assertThat(latestConnectionFailed).isFalse()
+ assertThat(latest).isFalse()
// ... but expires after 2s
advanceTimeBy(1001)
- assertThat(latestConnectionFailed).isTrue()
-
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun dataSwitch_whileAlreadyForcingValidation_resetsClock() =
testScope.runTest {
- var latestConnectionFailed: Boolean? = null
- val job =
- underTest.isDefaultConnectionFailed
- .onEach { latestConnectionFailed = it }
- .launchIn(this)
+ val latest by collectLastValue(underTest.isDefaultConnectionFailed)
connectionsRepository.mobileIsDefault.value = true
connectionsRepository.defaultConnectionIsValidated.value = true
+ runCurrent()
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
@@ -869,44 +845,37 @@
// WHEN another change in same group event happens
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
connectionsRepository.defaultConnectionIsValidated.value = false
+ runCurrent()
// THEN the forced validation remains for exactly 2 more seconds from now
// 1.500s from second event
advanceTimeBy(1500)
- assertThat(latestConnectionFailed).isFalse()
+ assertThat(latest).isFalse()
// 2.001s from the second event
advanceTimeBy(501)
- assertThat(latestConnectionFailed).isTrue()
-
- job.cancel()
+ assertThat(latest).isTrue()
}
@Test
fun isForceHidden_repoHasMobileHidden_true() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isForceHidden)
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
assertThat(latest).isTrue()
-
- job.cancel()
}
@Test
fun isForceHidden_repoDoesNotHaveMobileHidden_false() =
testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+ val latest by collectLastValue(underTest.isForceHidden)
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
assertThat(latest).isFalse()
-
- job.cancel()
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index cfa734a1..ab10bc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -47,7 +47,7 @@
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(ParameterizedAndroidJunit4::class)
-class KeyguardStatusBarViewModelTest(flags: FlagsParameterization?) : SysuiTestCase() {
+class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
@@ -66,7 +66,7 @@
}
init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 56e5e29..aac3640 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -138,7 +138,6 @@
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt
index 4085b1b..923b636 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityRepository.kt
@@ -25,8 +25,9 @@
@SysUISingleton
class FakeAccessibilityRepository(
override val isTouchExplorationEnabled: MutableStateFlow<Boolean>,
+ override val isEnabled: MutableStateFlow<Boolean>,
) : AccessibilityRepository {
- @Inject constructor() : this(MutableStateFlow(false))
+ @Inject constructor() : this(MutableStateFlow(false), MutableStateFlow(false))
}
@Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
index 66c9afb..b23767e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityTransitionAnimatorKosmos.kt
@@ -17,5 +17,13 @@
package com.android.systemui.animation
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
-val Kosmos.activityTransitionAnimator by Kosmos.Fixture { ActivityTransitionAnimator() }
+val Kosmos.activityTransitionAnimator by
+ Kosmos.Fixture {
+ ActivityTransitionAnimator(
+ // The main thread is checked in a bunch of places inside the different transitions
+ // animators, so we have to pass the real main executor here.
+ mainExecutor = testCase.context.mainExecutor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
index 77cb167..5a092f3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
@@ -19,7 +19,13 @@
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testCase
val Kosmos.dialogTransitionAnimator by Fixture {
- fakeDialogTransitionAnimator(interactionJankMonitor = interactionJankMonitor)
+ fakeDialogTransitionAnimator(
+ // The main thread is checked in a bunch of places inside the different transitions
+ // animators, so we have to pass the real main executor here.
+ mainExecutor = testCase.context.mainExecutor,
+ interactionJankMonitor = interactionJankMonitor,
+ )
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
index 48b72d0..1709329 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeDialogTransitionAnimator.kt
@@ -15,17 +15,20 @@
package com.android.systemui.animation
import com.android.internal.jank.InteractionJankMonitor
-import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
/** A [DialogTransitionAnimator] to be used in tests. */
@JvmOverloads
fun fakeDialogTransitionAnimator(
+ @Main mainExecutor: Executor,
isUnlocked: Boolean = true,
isShowingAlternateAuthOnUnlock: Boolean = false,
isPredictiveBackQsDialogAnim: Boolean = false,
interactionJankMonitor: InteractionJankMonitor,
): DialogTransitionAnimator {
return DialogTransitionAnimator(
+ mainExecutor = mainExecutor,
callback =
FakeCallback(
isUnlocked = isUnlocked,
@@ -36,7 +39,7 @@
object : AnimationFeatureFlags {
override val isPredictiveBackQsDialogAnim = isPredictiveBackQsDialogAnim
},
- transitionAnimator = fakeTransitionAnimator(),
+ transitionAnimator = fakeTransitionAnimator(mainExecutor),
isForTesting = true,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
index bc7ec3f..d07875f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/FakeTransitionAnimator.kt
@@ -15,10 +15,12 @@
package com.android.systemui.animation
import com.android.app.animation.Interpolators
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
/** A [TransitionAnimator] to be used in tests. */
-fun fakeTransitionAnimator(): TransitionAnimator {
- return TransitionAnimator(TEST_TIMINGS, TEST_INTERPOLATORS)
+fun fakeTransitionAnimator(@Main mainExecutor: Executor): TransitionAnimator {
+ return TransitionAnimator(mainExecutor, TEST_TIMINGS, TEST_INTERPOLATORS)
}
/**
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
index 3a61bf6..9b3482b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/data/repository/FakePackageChangeRepository.kt
@@ -18,8 +18,12 @@
import android.os.UserHandle
import com.android.systemui.common.shared.model.PackageChangeModel
+import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.util.time.SystemClock
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filter
class FakePackageChangeRepository(private val systemClock: SystemClock) : PackageChangeRepository {
@@ -31,6 +35,15 @@
user == UserHandle.ALL || user == UserHandle.getUserHandleForUid(it.packageUid)
}
+ private val _packageInstallSessions = MutableStateFlow<List<PackageInstallSession>>(emptyList())
+
+ override val packageInstallSessionsForPrimaryUser: Flow<List<PackageInstallSession>> =
+ _packageInstallSessions.asStateFlow()
+
+ fun setInstallSessions(sessions: List<PackageInstallSession>) {
+ _packageInstallSessions.value = sessions
+ }
+
suspend fun notifyChange(model: PackageChangeModel) {
_packageChanged.emit(model)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index 329c0f1..f7ce367 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -51,6 +51,7 @@
override fun abortRestoreWidgets() {}
private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) {
- _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority))
+ _communalWidgets.value +=
+ listOf(CommunalWidgetContentModel.Available(id, providerInfo, priority))
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt
index 29167d6..b38acc8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.statusbar.notification.domain.interactor.notificationLaunchAnimationInteractor
val Kosmos.windowManagerLockscreenVisibilityInteractor by
@@ -29,5 +30,6 @@
fromBouncerInteractor = fromPrimaryBouncerTransitionInteractor,
fromAlternateBouncerInteractor = fromAlternateBouncerTransitionInteractor,
notificationLaunchAnimationInteractor = notificationLaunchAnimationInteractor,
+ sceneInteractor = sceneInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
index 58b0ff8..67fa857 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import com.android.systemui.accessibility.domain.interactor.accessibilityInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntrySourceInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
@@ -49,6 +50,7 @@
keyguardViewController = { statusBarKeyguardViewManager },
deviceEntryInteractor = deviceEntryInteractor,
deviceEntrySourceInteractor = deviceEntrySourceInteractor,
+ accessibilityInteractor = accessibilityInteractor,
scope = testScope.backgroundScope,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index 3762497..ec56327 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -20,6 +20,7 @@
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -33,6 +34,7 @@
uiEventLogger,
{ interactionJankMonitor },
mock(),
+ { keyguardTransitionInteractor },
{ shadeInteractor },
{ deviceUnlockedInteractor },
{ sceneInteractor },
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
similarity index 97%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
index 302ac35..093ebd6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.qs.pipeline.domain.interactor
+package com.android.systemui.qs
import com.android.internal.logging.InstanceId
import com.android.systemui.animation.Expandable
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.kt
new file mode 100644
index 0000000..d686699
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/IconAndNameCustomRepositoryKosmos.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.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import android.content.packageManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.qs.pipeline.data.repository.installedTilesRepository
+import com.android.systemui.settings.userTracker
+import com.android.systemui.util.mockito.whenever
+
+val Kosmos.iconAndNameCustomRepository by
+ Kosmos.Fixture {
+ whenever(userTracker.userContext.packageManager).thenReturn(packageManager)
+ IconAndNameCustomRepository(
+ installedTilesRepository,
+ userTracker,
+ backgroundCoroutineContext,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt
new file mode 100644
index 0000000..ff33650
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/data/repository/StockTilesRepositoryKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+
+var Kosmos.stockTilesRepository by
+ Kosmos.Fixture {
+ testCase.context.orCreateTestableResources
+ StockTilesRepository(testCase.context.resources)
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt
new file mode 100644
index 0000000..bd54fd4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/domain/interactor/EditTilesListInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.panels.data.repository.iconAndNameCustomRepository
+import com.android.systemui.qs.panels.data.repository.stockTilesRepository
+import com.android.systemui.qs.tiles.viewmodel.qSTileConfigProvider
+
+val Kosmos.editTilesListInteractor by
+ Kosmos.Fixture {
+ EditTilesListInteractor(
+ stockTilesRepository,
+ qSTileConfigProvider,
+ iconAndNameCustomRepository,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt
new file mode 100644
index 0000000..612a5d9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModelKosmos.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.qs.panels.domain.interactor.editTilesListInteractor
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutMap
+import com.android.systemui.qs.panels.domain.interactor.gridLayoutTypeInteractor
+import com.android.systemui.qs.panels.domain.interactor.infiniteGridLayout
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
+import com.android.systemui.qs.pipeline.domain.interactor.minimumTilesInteractor
+
+val Kosmos.editModeViewModel by
+ Kosmos.Fixture {
+ EditModeViewModel(
+ editTilesListInteractor,
+ currentTilesInteractor,
+ minimumTilesInteractor,
+ infiniteGridLayout,
+ applicationCoroutineScope,
+ gridLayoutTypeInteractor,
+ gridLayoutMap,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt
index ff6b7d0..ed4c67e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/data/repository/FakeInstalledTilesComponentRepository.kt
@@ -17,23 +17,78 @@
package com.android.systemui.qs.pipeline.data.repository
import android.content.ComponentName
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ServiceInfo
+import android.graphics.drawable.Drawable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
class FakeInstalledTilesComponentRepository : InstalledTilesComponentRepository {
- private val installedComponentsPerUser =
- mutableMapOf<Int, MutableStateFlow<Set<ComponentName>>>()
+ private val installedServicesPerUser = mutableMapOf<Int, MutableStateFlow<List<ServiceInfo>>>()
override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> {
- return getFlow(userId).asStateFlow()
+ return getFlow(userId).map { it.map { it.componentName }.toSet() }
+ }
+
+ override fun getInstalledTilesServiceInfos(userId: Int): List<ServiceInfo> {
+ return getFlow(userId).value
}
fun setInstalledPackagesForUser(userId: Int, components: Set<ComponentName>) {
- getFlow(userId).value = components
+ getFlow(userId).value =
+ components.map {
+ ServiceInfo().apply {
+ packageName = it.packageName
+ name = it.className
+ applicationInfo = ApplicationInfo()
+ }
+ }
}
- private fun getFlow(userId: Int): MutableStateFlow<Set<ComponentName>> =
- installedComponentsPerUser.getOrPut(userId) { MutableStateFlow(emptySet()) }
+ fun setInstalledServicesForUser(userId: Int, services: List<ServiceInfo>) {
+ getFlow(userId).value = services.toList()
+ }
+
+ private fun getFlow(userId: Int): MutableStateFlow<List<ServiceInfo>> =
+ installedServicesPerUser.getOrPut(userId) { MutableStateFlow(emptyList()) }
+
+ companion object {
+ fun ServiceInfo(
+ componentName: ComponentName,
+ serviceName: String,
+ serviceIcon: Drawable? = null,
+ appName: String = "",
+ appIcon: Drawable? = null
+ ): ServiceInfo {
+ val appInfo =
+ object : ApplicationInfo() {
+ override fun loadLabel(pm: PackageManager): CharSequence {
+ return appName
+ }
+
+ override fun loadIcon(pm: PackageManager?): Drawable? {
+ return appIcon
+ }
+ }
+ val serviceInfo =
+ object : ServiceInfo() {
+ override fun loadLabel(pm: PackageManager): CharSequence {
+ return serviceName
+ }
+
+ override fun loadIcon(pm: PackageManager?): Drawable? {
+ return serviceIcon ?: getApplicationInfo().loadIcon(pm)
+ }
+ }
+ .apply {
+ packageName = componentName.packageName
+ name = componentName.className
+ applicationInfo = appInfo
+ }
+ return serviceInfo
+ }
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt
new file mode 100644
index 0000000..ef1189f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/domain/interactor/MinimumTilesInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.pipeline.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.pipeline.data.repository.minimumTilesRepository
+
+var Kosmos.minimumTilesInteractor by
+ Kosmos.Fixture { MinimumTilesInteractor(minimumTilesRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt
new file mode 100644
index 0000000..5c21ab6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/night/NightDisplayTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tiles.impl.night
+
+import com.android.systemui.accessibility.qs.QSAccessibilityModule
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+
+val Kosmos.qsNightDisplayTileConfig by
+ Kosmos.Fixture { QSAccessibilityModule.provideNightDisplayTileConfig(qsEventLogger) }
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 38ede44..ea02d0c 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
@@ -76,6 +76,16 @@
delegate.assertFlagValid()
delegate.programmaticCollapseShade()
}
+
+ fun setQsFullscreen(qsFullscreen: Boolean) {
+ delegate.assertFlagValid()
+ delegate.setQsFullscreen(qsFullscreen)
+ }
+
+ fun setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer: Boolean) {
+ delegate.assertFlagValid()
+ delegate.setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer)
+ }
}
/** Sets up shade state for tests for a specific value of the scene container flag. */
@@ -103,6 +113,10 @@
/** Sets the shade to half collapsed with no touch input. */
fun programmaticCollapseShade()
+
+ fun setQsFullscreen(qsFullscreen: Boolean)
+
+ fun setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer: Boolean)
}
/** Sets up shade state for tests when the scene container flag is disabled. */
@@ -146,6 +160,14 @@
shadeRepository.setLegacyShadeExpansion(.5f)
testScope.runCurrent()
}
+
+ override fun setQsFullscreen(qsFullscreen: Boolean) {
+ shadeRepository.legacyQsFullscreen.value = true
+ }
+
+ override fun setLegacyExpandedOrAwaitingInputTransfer(expanded: Boolean) {
+ shadeRepository.setLegacyExpandedOrAwaitingInputTransfer(expanded)
+ }
}
/** Sets up shade state for tests when the scene container flag is enabled. */
@@ -183,6 +205,16 @@
setTransitionProgress(Scenes.Shade, Scenes.Lockscreen, .5f, false)
}
+ override fun setQsFullscreen(qsFullscreen: Boolean) {
+ setQsExpansion(1f)
+ }
+
+ override fun setLegacyExpandedOrAwaitingInputTransfer(
+ legacyExpandedOrAwaitingInputTransfer: Boolean
+ ) {
+ setShadeExpansion(.1f)
+ }
+
override fun setLockscreenShadeExpansion(lockscreenShadeExpansion: Float) {
if (lockscreenShadeExpansion == 0f) {
setIdleScene(Scenes.Lockscreen)
diff --git a/ravenwood/scripts/convert-androidtest.py b/ravenwood/scripts/convert-androidtest.py
new file mode 100755
index 0000000..61ec54b
--- /dev/null
+++ b/ravenwood/scripts/convert-androidtest.py
@@ -0,0 +1,184 @@
+#!/usr/bin/python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This script converts a legacy test class (using AndroidTestCase, TestCase or
+# InstrumentationTestCase to a modern style test class, in a best-effort manner.
+#
+# Usage:
+# convert-androidtest.py TARGET-FILE [TARGET-FILE ...]
+#
+# Caveats:
+# - It adds all the extra imports, even if they're not needed.
+# - It won't sort imports.
+# - It also always adds getContext() and getTestContext().
+#
+
+import sys
+import fileinput
+import re
+import subprocess
+
+# Print message on console
+def log(msg):
+ print(msg, file=sys.stderr)
+
+
+# Matches `extends AndroidTestCase` (or another similar base class)
+re_extends = re.compile(
+ r''' \b extends \s+ (AndroidTestCase|TestCase|InstrumentationTestCase) \s* ''',
+ re.S + re.X)
+
+
+# Look into given files and return the files that have `re_extends`.
+def find_target_files(files):
+ ret = []
+
+ for file in files:
+ try:
+ with open(file, 'r') as f:
+ data = f.read()
+
+ if re_extends.search(data):
+ ret.append(file)
+
+ except FileNotFoundError as e:
+ log(f'Failed to open file {file}: {e}')
+
+ return ret
+
+
+def main(args):
+ files = args
+
+ # Find the files that should be processed.
+ files = find_target_files(files)
+
+ if len(files) == 0:
+ log("No target files found.")
+ return 0
+
+ # Process the files.
+ with fileinput.input(files=(files), inplace = True, backup = '.bak') as f:
+ import_seen = False
+ carry_over = ''
+ class_body_started = False
+ class_seen = False
+
+ def on_file_start():
+ nonlocal import_seen, carry_over, class_body_started, class_seen
+ import_seen = False
+ carry_over = ''
+ class_body_started = False
+ class_seen = False
+
+ for line in f:
+ if (fileinput.filelineno() == 1):
+ log(f"Processing: {fileinput.filename()}")
+ on_file_start()
+
+ line = line.rstrip('\n')
+
+ # Carry over a certain line to the next line.
+ if re.search(r'''@Override\b''', line):
+ carry_over = carry_over + line + '\n'
+ continue
+
+ if carry_over:
+ line = carry_over + line
+ carry_over = ''
+
+
+ # Remove the base class from the class definition.
+ line = re_extends.sub('', line)
+
+ # Add a @RunWith.
+ if not class_seen and re.search(r'''\b class \b''', line, re.X):
+ class_seen = True
+ print("@RunWith(AndroidJUnit4.class)")
+
+
+ # Inject extra imports.
+ if not import_seen and re.search(r'''^import\b''', line):
+ import_seen = True
+ print("""\
+import android.content.Context;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertSame;
+import static junit.framework.TestCase.assertNotSame;
+import static junit.framework.TestCase.assertTrue;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertNull;
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.fail;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.junit.Test;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+""")
+
+ # Add @Test to the test methods.
+ if re.search(r'''^ \s* public \s* void \s* test''', line, re.X):
+ print(" @Test")
+
+ # Convert setUp/tearDown to @Before/@After.
+ if re.search(r''' ^\s+ ( \@Override \s+ ) ? (public|protected) \s+ void \s+ (setUp|tearDown) ''',
+ line, re.X):
+ if re.search('setUp', line):
+ print(' @Before')
+ else:
+ print(' @After')
+
+ line = re.sub(r''' \s* \@Override \s* \n ''', '', line, 0, re.X)
+ line = re.sub(r'''protected''', 'public', line, 0, re.X)
+
+ # Remove the super setUp / tearDown call.
+ if re.search(r''' \b super \. (setUp|tearDown) \b ''', line, re.X):
+ continue
+
+ # Convert mContext to getContext().
+ line = re.sub(r'''\b mContext \b ''', 'getContext()', line, 0, re.X)
+
+ # Print the processed line.
+ print(line)
+
+ # Add getContext() / getTestContext() at the beginning of the class.
+ if not class_body_started and re.search(r'''\{''', line):
+ class_body_started = True
+ print("""\
+ private Context getContext() {
+ return InstrumentationRegistry.getInstrumentation().getTargetContext();
+ }
+
+ private Context getTestContext() {
+ return InstrumentationRegistry.getInstrumentation().getContext();
+ }
+""")
+
+
+ # Run diff
+ for file in files:
+ subprocess.call(["diff", "-u", "--color=auto", f"{file}.bak", file])
+
+ log(f'{len(files)} file(s) converted.')
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index 2f54f8c..2a7458f 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -1469,10 +1469,7 @@
int policyFlags = mState.getLastReceivedPolicyFlags();
if (mState.isDragging()) {
// Send an event to the end of the drag gesture.
- int pointerIdBits = ALL_POINTER_ID_BITS;
- if (Flags.fixDragPointerWhenEndingDrag()) {
- pointerIdBits = 1 << mDraggingPointerId;
- }
+ int pointerIdBits = 1 << mDraggingPointerId;
mDispatcher.sendMotionEvent(event, ACTION_UP, rawEvent, pointerIdBits, policyFlags);
}
mState.startDelegating();
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index e830523..249b3cb 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -83,6 +83,7 @@
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Binder;
+import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
@@ -172,6 +173,7 @@
private static final String TAG = "AppWidgetServiceImpl";
private static final boolean DEBUG = false;
+ private static final boolean DEBUG_NULL_PROVIDER_INFO = Build.IS_DEBUGGABLE;
private static final String OLD_KEYGUARD_HOST_PACKAGE = "android";
private static final String NEW_KEYGUARD_HOST_PACKAGE = "com.android.keyguard";
@@ -736,7 +738,10 @@
}
RemoteViews views = new RemoteViews(mContext.getPackageName(),
R.layout.work_widget_mask_view);
- ApplicationInfo appInfo = provider.info.providerInfo.applicationInfo;
+ final ActivityInfo activityInfo = provider.info.providerInfo;
+ final ApplicationInfo appInfo = activityInfo != null ? activityInfo.applicationInfo : null;
+ final String packageName = appInfo != null
+ ? appInfo.packageName : provider.id.componentName.getPackageName();
final int appUserId = provider.getUserId();
boolean showBadge = false;
@@ -750,7 +755,7 @@
} else if (provider.maskedBySuspendedPackage) {
showBadge = mUserManager.hasBadge(appUserId);
final UserPackage suspendingPackage = mPackageManagerInternal.getSuspendingPackage(
- appInfo.packageName, appUserId);
+ packageName, appUserId);
// TODO(b/281839596): don't rely on platform always meaning suspended by admin.
if (suspendingPackage != null
&& PLATFORM_PACKAGE_NAME.equals(suspendingPackage.packageName)) {
@@ -759,11 +764,11 @@
} else {
final SuspendDialogInfo dialogInfo =
mPackageManagerInternal.getSuspendedDialogInfo(
- appInfo.packageName, suspendingPackage, appUserId);
+ packageName, suspendingPackage, appUserId);
// onUnsuspend is null because we don't want to start any activity on
// unsuspending from a suspended widget.
onClickIntent = SuspendedAppActivity.createSuspendedAppInterceptIntent(
- appInfo.packageName, suspendingPackage, dialogInfo, null, null,
+ packageName, suspendingPackage, dialogInfo, null, null,
appUserId);
}
} else if (provider.maskedByLockedProfile) {
@@ -778,7 +783,7 @@
showBadge = mUserManager.hasBadge(appUserId);
}
- Icon icon = appInfo.icon != 0
+ Icon icon = (appInfo != null && appInfo.icon != 0)
? Icon.createWithResource(appInfo.packageName, appInfo.icon)
: Icon.createWithResource(mContext, android.R.drawable.sym_def_app_icon);
views.setImageViewIcon(R.id.work_widget_app_icon, icon);
@@ -2955,6 +2960,9 @@
AppWidgetProviderInfo info = new AppWidgetProviderInfo();
info.provider = providerId.componentName;
info.providerInfo = ri.activityInfo;
+ if (DEBUG_NULL_PROVIDER_INFO) {
+ Objects.requireNonNull(ri.activityInfo);
+ }
return info;
}
return null;
@@ -2989,6 +2997,9 @@
AppWidgetProviderInfo info = new AppWidgetProviderInfo();
info.provider = providerId.componentName;
info.providerInfo = activityInfo;
+ if (DEBUG_NULL_PROVIDER_INFO) {
+ Objects.requireNonNull(activityInfo);
+ }
final Resources resources;
final long identity = Binder.clearCallingIdentity();
@@ -3564,6 +3575,9 @@
AppWidgetProviderInfo info = new AppWidgetProviderInfo();
info.provider = providerId.componentName;
info.providerInfo = providerInfo;
+ if (DEBUG_NULL_PROVIDER_INFO) {
+ Objects.requireNonNull(providerInfo);
+ }
provider = new Provider();
provider.setPartialInfoLocked(info);
@@ -3580,6 +3594,9 @@
if (info != null) {
info.provider = providerId.componentName;
info.providerInfo = providerInfo;
+ if (DEBUG_NULL_PROVIDER_INFO) {
+ Objects.requireNonNull(providerInfo);
+ }
provider.setInfoLocked(info);
}
}
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index 9b72288..d7c65c7 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -41,7 +41,6 @@
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
-import android.view.Display;
import android.view.InputDevice;
import android.view.WindowManager;
@@ -169,7 +168,6 @@
createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId,
deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys));
- setVirtualMousePointerDisplayId(displayId);
}
void createTouchscreen(@NonNull String deviceName, int vendorId, int productId,
@@ -236,15 +234,6 @@
if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_KEYBOARD) {
mInputManagerInternal.removeKeyboardLayoutAssociation(phys);
}
-
- // Reset values to the default if all virtual mice are unregistered, or set display
- // id if there's another mouse (choose the most recent). The inputDeviceDescriptor must be
- // removed from the mInputDeviceDescriptors instance variable prior to this point.
- if (inputDeviceDescriptor.isMouse()) {
- if (getVirtualMousePointerDisplayId() == inputDeviceDescriptor.getDisplayId()) {
- updateActivePointerDisplayIdLocked();
- }
- }
}
/**
@@ -276,29 +265,6 @@
mWindowManager.setDisplayImePolicy(displayId, policy);
}
- // TODO(b/293587049): Remove after pointer icon refactor is complete.
- @GuardedBy("mLock")
- private void updateActivePointerDisplayIdLocked() {
- InputDeviceDescriptor mostRecentlyCreatedMouse = null;
- for (int i = 0; i < mInputDeviceDescriptors.size(); ++i) {
- InputDeviceDescriptor otherInputDeviceDescriptor = mInputDeviceDescriptors.valueAt(i);
- if (otherInputDeviceDescriptor.isMouse()) {
- if (mostRecentlyCreatedMouse == null
- || (otherInputDeviceDescriptor.getCreationOrderNumber()
- > mostRecentlyCreatedMouse.getCreationOrderNumber())) {
- mostRecentlyCreatedMouse = otherInputDeviceDescriptor;
- }
- }
- }
- if (mostRecentlyCreatedMouse != null) {
- setVirtualMousePointerDisplayId(
- mostRecentlyCreatedMouse.getDisplayId());
- } else {
- // All mice have been unregistered
- setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY);
- }
- }
-
/**
* Validates a device name by checking whether a device with the same name already exists.
* @param deviceName The name of the device to be validated
@@ -355,9 +321,6 @@
if (inputDeviceDescriptor == null) {
return false;
}
- if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
- setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
- }
return mNativeWrapper.writeButtonEvent(inputDeviceDescriptor.getNativePointer(),
event.getButtonCode(), event.getAction(), event.getEventTimeNanos());
}
@@ -384,9 +347,6 @@
if (inputDeviceDescriptor == null) {
return false;
}
- if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
- setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
- }
return mNativeWrapper.writeRelativeEvent(inputDeviceDescriptor.getNativePointer(),
event.getRelativeX(), event.getRelativeY(), event.getEventTimeNanos());
}
@@ -399,9 +359,6 @@
if (inputDeviceDescriptor == null) {
return false;
}
- if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
- setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
- }
return mNativeWrapper.writeScrollEvent(inputDeviceDescriptor.getNativePointer(),
event.getXAxisMovement(), event.getYAxisMovement(), event.getEventTimeNanos());
}
@@ -415,9 +372,6 @@
throw new IllegalArgumentException(
"Could not get cursor position for input device for given token");
}
- if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) {
- setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId());
- }
return LocalServices.getService(InputManagerInternal.class).getCursorPosition(
inputDeviceDescriptor.getDisplayId());
}
@@ -878,22 +832,4 @@
/** Returns true if the calling thread is a valid thread for device creation. */
boolean isValidThread();
}
-
- // TODO(b/293587049): Remove after pointer icon refactor is complete.
- private void setVirtualMousePointerDisplayId(int displayId) {
- if (com.android.input.flags.Flags.enablePointerChoreographer()) {
- // We no longer need to set the pointer display when pointer choreographer is enabled.
- return;
- }
- mInputManagerInternal.setVirtualMousePointerDisplayId(displayId);
- }
-
- // TODO(b/293587049): Remove after pointer icon refactor is complete.
- private int getVirtualMousePointerDisplayId() {
- if (com.android.input.flags.Flags.enablePointerChoreographer()) {
- // We no longer need to get the pointer display when pointer choreographer is enabled.
- return Display.INVALID_DISPLAY;
- }
- return mInputManagerInternal.getVirtualMousePointerDisplayId();
- }
}
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 3ff0504..d153c18 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -258,6 +258,8 @@
"core_os_flags_lib",
"connectivity_flags_lib",
"dreams_flags_lib",
+ "aconfig_new_storage_flags_lib",
+ "aconfigd_java_proto_lib",
],
javac_shard_size: 50,
javacflags: [
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 4ca9e33..bef5c612 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -1168,9 +1168,7 @@
}
private boolean shouldAllowBootCompletedStart(ServiceRecord r, int foregroundServiceType) {
- @PowerExemptionManager.ReasonCode final int fgsStartReasonCode =
- r.mInfoTempFgsAllowListReason != null ? r.mInfoTempFgsAllowListReason.mReasonCode
- : REASON_DENIED;
+ @PowerExemptionManager.ReasonCode final int fgsStartReasonCode = r.getFgsAllowStart();
if (Flags.fgsBootCompleted()
&& CompatChanges.isChangeEnabled(FGS_BOOT_COMPLETED_RESTRICTIONS, r.appInfo.uid)
&& fgsStartReasonCode == PowerExemptionManager.REASON_BOOT_COMPLETED) {
@@ -2454,10 +2452,19 @@
} else if (lastTimeOutAt > 0) {
// Time limit was exhausted within the past 24 hours and the app
// has not been in the TOP state since then, throw an exception.
- throw new ForegroundServiceStartNotAllowedException("Time limit"
- + " already exhausted for foreground service type "
+ final String exceptionMsg = "Time limit already exhausted for"
+ + " foreground service type "
+ ServiceInfo.foregroundServiceTypeToLabel(
- foregroundServiceType));
+ foregroundServiceType);
+ if (!android.app.Flags.gateFgsTimeoutAnrBehavior()) {
+ throw new ForegroundServiceStartNotAllowedException(
+ exceptionMsg);
+ } else {
+ // Only throw an exception above while the new ANR behavior
+ // is not gated, otherwise, reset the limit temporarily.
+ Slog.wtf(TAG, exceptionMsg);
+ fgsTypeInfo.reset();
+ }
}
}
} else {
@@ -3943,6 +3950,12 @@
+ ServiceInfo.foregroundServiceTypeToLabel(fgsType)
+ " did not stop within its timeout: " + sr.getComponentName();
+ if (android.app.Flags.gateFgsTimeoutAnrBehavior()) {
+ // Log a WTF instead of throwing an ANR while the new behavior is gated.
+ Slog.wtf(TAG, reason);
+ return;
+ }
+
final TimeoutRecord tr = TimeoutRecord.forFgsTimeout(reason);
tr.mLatencyTracker.waitingOnAMSLockStarted();
synchronized (mAm) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 3cea014..a182a10 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -115,6 +115,7 @@
import android.util.ArraySet;
import android.util.DebugUtils;
import android.util.DisplayMetrics;
+import android.util.SparseArray;
import android.util.TeeWriter;
import android.util.proto.ProtoOutputStream;
import android.view.Choreographer;
@@ -275,9 +276,9 @@
case "compact":
return runCompact(pw);
case "freeze":
- return runFreeze(pw);
+ return runFreeze(pw, true);
case "unfreeze":
- return runUnfreeze(pw);
+ return runFreeze(pw, false);
case "instrument":
getOutPrintWriter().println("Error: must be invoked through 'am instrument'.");
return -1;
@@ -1203,45 +1204,27 @@
}
@NeverCompile
- int runFreeze(PrintWriter pw) throws RemoteException {
+ int runFreeze(PrintWriter pw, boolean freeze) throws RemoteException {
String freezerOpt = getNextOption();
boolean isSticky = false;
- if (freezerOpt != null) {
- isSticky = freezerOpt.equals("--sticky");
- }
- ProcessRecord app = getProcessFromShell();
- if (app == null) {
- getErrPrintWriter().println("Error: could not find process");
- return -1;
- }
- pw.println("Freezing pid: " + app.mPid + " sticky=" + isSticky);
- synchronized (mInternal) {
- synchronized (mInternal.mProcLock) {
- app.mOptRecord.setFreezeSticky(isSticky);
- mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(app);
- }
- }
- return 0;
- }
- @NeverCompile
- int runUnfreeze(PrintWriter pw) throws RemoteException {
- String freezerOpt = getNextOption();
- boolean isSticky = false;
if (freezerOpt != null) {
isSticky = freezerOpt.equals("--sticky");
}
- ProcessRecord app = getProcessFromShell();
- if (app == null) {
- getErrPrintWriter().println("Error: could not find process");
+ ProcessRecord proc = getProcessFromShell();
+ if (proc == null) {
return -1;
}
- pw.println("Unfreezing pid: " + app.mPid);
+ pw.print(freeze ? "Freezing" : "Unfreezing");
+ pw.print(" process " + proc.processName);
+ pw.println(" (" + proc.mPid + ") sticky=" + isSticky);
synchronized (mInternal) {
synchronized (mInternal.mProcLock) {
- synchronized (mInternal.mOomAdjuster.mCachedAppOptimizer.mFreezerLock) {
- app.mOptRecord.setFreezeSticky(isSticky);
- mInternal.mOomAdjuster.mCachedAppOptimizer.unfreezeAppInternalLSP(app, 0,
+ proc.mOptRecord.setFreezeSticky(isSticky);
+ if (freeze) {
+ mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(proc);
+ } else {
+ mInternal.mOomAdjuster.mCachedAppOptimizer.unfreezeAppInternalLSP(proc, 0,
true);
}
}
@@ -1250,43 +1233,42 @@
}
/**
- * Parses from the shell the process name and user id if provided and provides the corresponding
- * {@link ProcessRecord)} If no user is provided, it will fallback to current user.
- * Example usage: {@code <processname> --user current} or {@code <processname>}
- * @return process record of process, null if none found.
+ * Parses from the shell the pid or process name and provides the corresponding
+ * {@link ProcessRecord}.
+ * Example usage: {@code <processname>} or {@code <pid>}
+ * @return process record of process, null if none or more than one found.
* @throws RemoteException
*/
@NeverCompile
ProcessRecord getProcessFromShell() throws RemoteException {
- ProcessRecord app;
- String processName = getNextArgRequired();
- synchronized (mInternal.mProcLock) {
- // Default to current user
- int userId = getUserIdFromShellOrFallback();
- final int uid =
- mInternal.getPackageManagerInternal().getPackageUid(processName, 0, userId);
- app = mInternal.getProcessRecordLocked(processName, uid);
+ ProcessRecord proc = null;
+ String process = getNextArgRequired();
+ try {
+ int pid = Integer.parseInt(process);
+ synchronized (mInternal.mPidsSelfLocked) {
+ proc = mInternal.mPidsSelfLocked.get(pid);
+ }
+ } catch (NumberFormatException e) {
+ // Fallback to process name if it's not a valid pid
}
- return app;
- }
- /**
- * @return User id from command line provided in the form of
- * {@code --user <userid|current|all>} and if the argument is not found it will fallback
- * to current user.
- * @throws RemoteException
- */
- @NeverCompile
- int getUserIdFromShellOrFallback() throws RemoteException {
- int userId = mInterface.getCurrentUserId();
- String userOpt = getNextOption();
- if (userOpt != null && "--user".equals(userOpt)) {
- int inputUserId = UserHandle.parseUserArg(getNextArgRequired());
- if (inputUserId != UserHandle.USER_CURRENT) {
- userId = inputUserId;
+ if (proc == null) {
+ synchronized (mInternal.mProcLock) {
+ ArrayMap<String, SparseArray<ProcessRecord>> all =
+ mInternal.mProcessList.getProcessNamesLOSP().getMap();
+ SparseArray<ProcessRecord> procs = all.get(process);
+ if (procs == null || procs.size() == 0) {
+ getErrPrintWriter().println("Error: could not find process");
+ return null;
+ } else if (procs.size() > 1) {
+ getErrPrintWriter().println("Error: more than one processes found");
+ return null;
+ }
+ proc = procs.valueAt(0);
}
}
- return userId;
+
+ return proc;
}
int runDumpHeap(PrintWriter pw) throws RemoteException {
@@ -4306,24 +4288,26 @@
pw.println(" --allow-background-activity-starts: The receiver may start activities");
pw.println(" even if in the background.");
pw.println(" --async: Send without waiting for the completion of the receiver.");
- pw.println(" compact [some|full] <process_name> [--user <USER_ID>]");
- pw.println(" Perform a single process compaction.");
+ pw.println(" compact {some|full} <PROCESS>");
+ pw.println(" Perform a single process compaction. The given <PROCESS> argument");
+ pw.println(" may be either a process name or pid.");
pw.println(" some: execute file compaction.");
pw.println(" full: execute anon + file compaction.");
- pw.println(" system: system compaction.");
pw.println(" compact system");
pw.println(" Perform a full system compaction.");
- pw.println(" compact native [some|full] <pid>");
+ pw.println(" compact native {some|full} <pid>");
pw.println(" Perform a native compaction for process with <pid>.");
pw.println(" some: execute file compaction.");
pw.println(" full: execute anon + file compaction.");
- pw.println(" freeze [--sticky] <processname> [--user <USER_ID>]");
- pw.println(" Freeze a process.");
- pw.println(" --sticky: persists the frozen state for the process lifetime or");
+ pw.println(" freeze [--sticky] <PROCESS>");
+ pw.println(" Freeze a process. The given <PROCESS> argument");
+ pw.println(" may be either a process name or pid. Options are:");
+ pw.println(" --sticky: persists the frozen state for the process lifetime or");
pw.println(" until an unfreeze is triggered via shell");
- pw.println(" unfreeze [--sticky] <processname> [--user <USER_ID>]");
- pw.println(" Unfreeze a process.");
- pw.println(" --sticky: persists the unfrozen state for the process lifetime or");
+ pw.println(" unfreeze [--sticky] <PROCESS>");
+ pw.println(" Unfreeze a process. The given <PROCESS> argument");
+ pw.println(" may be either a process name or pid. Options are:");
+ pw.println(" --sticky: persists the unfrozen state for the process lifetime or");
pw.println(" until a freeze is triggered via shell");
pw.println(" instrument [-r] [-e <NAME> <VALUE>] [-p <FILE>] [-w]");
pw.println(" [--user <USER_ID> | current]");
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 9520621..827db57 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -20,6 +20,8 @@
import android.content.ContentResolver;
import android.database.ContentObserver;
import android.net.Uri;
+import android.net.LocalSocketAddress;
+import android.net.LocalSocket;
import android.os.AsyncTask;
import android.os.Build;
import android.os.SystemProperties;
@@ -30,6 +32,14 @@
import com.android.internal.annotations.VisibleForTesting;
+import android.aconfigd.Aconfigd.StorageRequestMessage;
+import android.aconfigd.Aconfigd.StorageRequestMessages;
+import android.aconfigd.Aconfigd.StorageReturnMessage;
+import android.aconfigd.Aconfigd.StorageReturnMessages;
+import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
@@ -185,6 +195,7 @@
"pmw",
"power",
"preload_safety",
+ "printing",
"privacy_infra_policy",
"resource_manager",
"responsible_apis",
@@ -224,6 +235,8 @@
public static final String NAMESPACE_REBOOT_STAGING = "staged";
public static final String NAMESPACE_REBOOT_STAGING_DELIMITER = "*";
+ public static final String NAMESPACE_LOCAL_OVERRIDES = "device_config_overrides";
+
private final String[] mGlobalSettings;
private final String[] mDeviceConfigScopes;
@@ -329,6 +342,7 @@
HashMap<String, HashMap<String, String>> propsToStage =
getStagedFlagsWithValueChange(properties);
+ // send prop stage request to sys prop
for (HashMap.Entry<String, HashMap<String, String>> entry : propsToStage.entrySet()) {
String actualNamespace = entry.getKey();
HashMap<String, String> flagValuesToStage = entry.getValue();
@@ -349,7 +363,118 @@
}
}
- });
+ // send prop stage request to new storage
+ if (enableAconfigStorageDaemon()) {
+ stageFlagsInNewStorage(propsToStage);
+ }
+
+ });
+
+ // add prop sync callback for flag local overrides
+ DeviceConfig.addOnPropertiesChangedListener(
+ NAMESPACE_LOCAL_OVERRIDES,
+ AsyncTask.THREAD_POOL_EXECUTOR,
+ (DeviceConfig.Properties properties) -> {
+ if (enableAconfigStorageDaemon()) {
+ setLocalOverridesInNewStorage(properties);
+ }
+ });
+ }
+
+ /**
+ * apply flag local override in aconfig new storage
+ * @param props
+ * @return aconfigd socket return
+ */
+ public static StorageReturnMessages sendAconfigdRequests(StorageRequestMessages requests) {
+ // connect to aconfigd socket
+ LocalSocket client = new LocalSocket();
+ try{
+ client.connect(new LocalSocketAddress(
+ "aconfigd", LocalSocketAddress.Namespace.RESERVED));
+ log("connected to aconfigd socket");
+ } catch (IOException ioe) {
+ log("failed to connect to aconfigd socket", ioe);
+ return null;
+ }
+
+ DataInputStream inputStream = null;
+ DataOutputStream outputStream = null;
+ try {
+ inputStream = new DataInputStream(client.getInputStream());
+ outputStream = new DataOutputStream(client.getOutputStream());
+ } catch (IOException ioe) {
+ log("failed to get local socket iostreams", ioe);
+ return null;
+ }
+
+ // send requests
+ try {
+ byte[] requests_bytes = requests.toByteArray();
+ outputStream.writeInt(requests_bytes.length);
+ outputStream.write(requests_bytes, 0, requests_bytes.length);
+ log(requests.getMsgsCount() + " flag override requests sent to aconfigd");
+ } catch (IOException ioe) {
+ log("failed to send requests to aconfigd", ioe);
+ return null;
+ }
+
+ // read return
+ StorageReturnMessages return_msgs = null;
+ try {
+ int num_bytes = inputStream.readInt();
+ byte[] buffer = new byte[num_bytes];
+ inputStream.read(buffer, 0, num_bytes);
+ return_msgs = StorageReturnMessages.parseFrom(buffer);
+ log(return_msgs.getMsgsCount() + " acknowledgement received from aconfigd");
+ } catch (IOException ioe) {
+ log("failed to read requests return from aconfigd", ioe);
+ return null;
+ }
+
+ return return_msgs;
+ }
+
+ /**
+ * apply flag local override in aconfig new storage
+ * @param props
+ */
+ public static void setLocalOverridesInNewStorage(DeviceConfig.Properties props) {
+ StorageRequestMessages.Builder requests_builder = StorageRequestMessages.newBuilder();
+ for (String flagName : props.getKeyset()) {
+ String flagValue = props.getString(flagName, null);
+ if (flagName == null || flagValue == null) {
+ continue;
+ }
+
+ int idx = flagName.indexOf(":");
+ if (idx == -1 || idx == flagName.length() - 1 || idx == 0) {
+ log("invalid local flag override: " + flagName);
+ continue;
+ }
+ String actualNamespace = flagName.substring(0, idx);
+ String fullFlagName = flagName.substring(idx+1);
+ idx = fullFlagName.lastIndexOf(".");
+ if (idx == -1) {
+ log("invalid flag name: " + fullFlagName);
+ continue;
+ }
+ String packageName = fullFlagName.substring(0, idx);
+ String realFlagName = fullFlagName.substring(idx+1);
+
+ StorageRequestMessage.FlagOverrideMessage.Builder override_msg_builder =
+ StorageRequestMessage.FlagOverrideMessage.newBuilder();
+ override_msg_builder.setPackageName(packageName);
+ override_msg_builder.setFlagName(realFlagName);
+ override_msg_builder.setFlagValue(flagValue);
+ override_msg_builder.setIsLocal(true);
+
+ StorageRequestMessage.Builder request_builder = StorageRequestMessage.newBuilder();
+ request_builder.setFlagOverrideMessage(override_msg_builder.build());
+ requests_builder.addMsgs(request_builder.build());
+ }
+ StorageRequestMessages requests = requests_builder.build();
+ StorageReturnMessages acks = sendAconfigdRequests(requests);
}
public static SettingsToPropertiesMapper start(ContentResolver contentResolver) {
@@ -421,6 +546,43 @@
}
/**
+ * stage flags in aconfig new storage
+ * @param propsToStage
+ */
+ @VisibleForTesting
+ static void stageFlagsInNewStorage(HashMap<String, HashMap<String, String>> propsToStage) {
+ // create storage request proto
+ StorageRequestMessages.Builder requests_builder = StorageRequestMessages.newBuilder();
+ for (HashMap.Entry<String, HashMap<String, String>> entry : propsToStage.entrySet()) {
+ String actualNamespace = entry.getKey();
+ HashMap<String, String> flagValuesToStage = entry.getValue();
+ for (String fullFlagName : flagValuesToStage.keySet()) {
+ String stagedValue = flagValuesToStage.get(fullFlagName);
+ int idx = fullFlagName.lastIndexOf(".");
+ if (idx == -1) {
+ log("invalid flag name: " + fullFlagName);
+ continue;
+ }
+ String packageName = fullFlagName.substring(0, idx);
+ String flagName = fullFlagName.substring(idx+1);
+
+ StorageRequestMessage.FlagOverrideMessage.Builder override_msg_builder =
+ StorageRequestMessage.FlagOverrideMessage.newBuilder();
+ override_msg_builder.setPackageName(packageName);
+ override_msg_builder.setFlagName(flagName);
+ override_msg_builder.setFlagValue(stagedValue);
+ override_msg_builder.setIsLocal(false);
+
+ StorageRequestMessage.Builder request_builder = StorageRequestMessage.newBuilder();
+ request_builder.setFlagOverrideMessage(override_msg_builder.build());
+ requests_builder.addMsgs(request_builder.build());
+ }
+ }
+ StorageRequestMessages requests = requests_builder.build();
+ StorageReturnMessages acks = sendAconfigdRequests(requests);
+ }
+
+ /**
* system property name constructing rule for aconfig flags:
* "persist.device_config.aconfig_flags.[category_name].[flag_name]".
* If the name contains invalid characters or substrings for system property name,
@@ -483,10 +645,10 @@
for (String flagName : flagStagedValues.keySet()) {
String stagedValue = flagStagedValues.get(flagName);
String currentValue = flagCurrentValues.get(flagName);
- if (currentValue == null) {
- currentValue = new String("false");
+ if (stagedValue == null) {
+ continue;
}
- if (stagedValue != null && !stagedValue.equalsIgnoreCase(currentValue)) {
+ if (currentValue == null || !stagedValue.equalsIgnoreCase(currentValue)) {
flagsToStage.put(flagName, stagedValue);
}
}
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 6308652..85eb044 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -1255,7 +1255,9 @@
for (int uidIdx = mUidStates.size() - 1; uidIdx >= 0; uidIdx--) {
int uid = mUidStates.keyAt(uidIdx);
if (knownUids.get(uid, false)) {
- if (uid >= Process.FIRST_APPLICATION_UID) {
+ int appId = UserHandle.getAppId(uid);
+ if (appId >= Process.FIRST_APPLICATION_UID
+ && appId <= Process.LAST_APPLICATION_UID) {
ArrayMap<String, Ops> pkgOps = mUidStates.valueAt(uidIdx).pkgOps;
for (int pkgIdx = pkgOps.size() - 1; pkgIdx >= 0; pkgIdx--) {
String pkgName = pkgOps.keyAt(pkgIdx);
diff --git a/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java b/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java
new file mode 100644
index 0000000..3e8acee
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/BiometricDanglingReceiver.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.biometrics;
+
+import static android.content.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.biometrics.BiometricsProtoEnums;
+import android.provider.Settings;
+import android.util.Slog;
+
+import com.android.server.biometrics.sensors.BiometricNotificationUtils;
+
+/**
+ * Receives broadcast to biometrics dangling notification.
+ */
+public class BiometricDanglingReceiver extends BroadcastReceiver {
+ private static final String TAG = "BiometricDanglingReceiver";
+
+ public static final String ACTION_FINGERPRINT_RE_ENROLL_LAUNCH =
+ "action_fingerprint_re_enroll_launch";
+ public static final String ACTION_FINGERPRINT_RE_ENROLL_DISMISS =
+ "action_fingerprint_re_enroll_dismiss";
+
+ public static final String ACTION_FACE_RE_ENROLL_LAUNCH =
+ "action_face_re_enroll_launch";
+ public static final String ACTION_FACE_RE_ENROLL_DISMISS =
+ "action_face_re_enroll_dismiss";
+
+ public static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS";
+
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+
+ /**
+ * Constructor for BiometricDanglingReceiver.
+ *
+ * @param context context
+ * @param modality the value from BiometricsProtoEnums.MODALITY_*
+ */
+ public BiometricDanglingReceiver(@NonNull Context context, int modality) {
+ final IntentFilter intentFilter = new IntentFilter();
+ if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
+ intentFilter.addAction(ACTION_FINGERPRINT_RE_ENROLL_LAUNCH);
+ intentFilter.addAction(ACTION_FINGERPRINT_RE_ENROLL_DISMISS);
+ } else if (modality == BiometricsProtoEnums.MODALITY_FACE) {
+ intentFilter.addAction(ACTION_FACE_RE_ENROLL_LAUNCH);
+ intentFilter.addAction(ACTION_FACE_RE_ENROLL_DISMISS);
+ }
+ context.registerReceiver(this, intentFilter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Slog.d(TAG, "Received: " + intent.getAction());
+ if (ACTION_FINGERPRINT_RE_ENROLL_LAUNCH.equals(intent.getAction())) {
+ launchBiometricEnrollActivity(context, Settings.ACTION_FINGERPRINT_ENROLL);
+ BiometricNotificationUtils.cancelFingerprintReEnrollNotification(context);
+ } else if (ACTION_FINGERPRINT_RE_ENROLL_DISMISS.equals(intent.getAction())) {
+ BiometricNotificationUtils.cancelFingerprintReEnrollNotification(context);
+ } else if (ACTION_FACE_RE_ENROLL_LAUNCH.equals(intent.getAction())) {
+ launchBiometricEnrollActivity(context, FACE_SETTINGS_ACTION);
+ BiometricNotificationUtils.cancelFaceReEnrollNotification(context);
+ } else if (ACTION_FACE_RE_ENROLL_DISMISS.equals(intent.getAction())) {
+ BiometricNotificationUtils.cancelFaceReEnrollNotification(context);
+ }
+ context.unregisterReceiver(this);
+ }
+
+ private void launchBiometricEnrollActivity(Context context, String action) {
+ context.sendBroadcast(new Intent(ACTION_CLOSE_SYSTEM_DIALOGS));
+ final Intent intent = new Intent(action);
+ intent.setPackage(SETTINGS_PACKAGE);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
index 0e22f75..eaa5e2a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
@@ -24,13 +24,18 @@
import android.content.Context;
import android.content.Intent;
import android.hardware.biometrics.BiometricManager;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceEnrollOptions;
import android.hardware.fingerprint.FingerprintEnrollOptions;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.text.BidiFormatter;
import android.util.Slog;
import com.android.internal.R;
+import com.android.server.biometrics.BiometricDanglingReceiver;
+
+import java.util.List;
/**
* Biometric notification helper class.
@@ -39,6 +44,7 @@
private static final String TAG = "BiometricNotificationUtils";
private static final String FACE_RE_ENROLL_NOTIFICATION_TAG = "FaceReEnroll";
+ private static final String FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG = "FingerprintReEnroll";
private static final String BAD_CALIBRATION_NOTIFICATION_TAG = "FingerprintBadCalibration";
private static final String KEY_RE_ENROLL_FACE = "re_enroll_face_unlock";
private static final String FACE_SETTINGS_ACTION = "android.settings.FACE_SETTINGS";
@@ -50,6 +56,8 @@
private static final String FACE_ENROLL_CHANNEL = "FaceEnrollNotificationChannel";
private static final String FACE_RE_ENROLL_CHANNEL = "FaceReEnrollNotificationChannel";
private static final String FINGERPRINT_ENROLL_CHANNEL = "FingerprintEnrollNotificationChannel";
+ private static final String FINGERPRINT_RE_ENROLL_CHANNEL =
+ "FingerprintReEnrollNotificationChannel";
private static final String FINGERPRINT_BAD_CALIBRATION_CHANNEL =
"FingerprintBadCalibrationNotificationChannel";
private static final long NOTIFICATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -177,10 +185,124 @@
BAD_CALIBRATION_NOTIFICATION_TAG, Notification.VISIBILITY_SECRET, false);
}
+ /**
+ * Shows a biometric re-enroll notification.
+ */
+ public static void showBiometricReEnrollNotification(@NonNull Context context,
+ @NonNull List<String> identifiers, boolean allIdentifiersDeleted, int modality) {
+ final boolean isFingerprint = modality == BiometricsProtoEnums.MODALITY_FINGERPRINT;
+ final String reEnrollName = isFingerprint ? FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG
+ : FACE_RE_ENROLL_NOTIFICATION_TAG;
+ if (identifiers.isEmpty()) {
+ Slog.v(TAG, "Skipping " + reEnrollName + " notification : empty list");
+ return;
+ }
+ Slog.d(TAG, "Showing " + reEnrollName + " notification :[" + identifiers.size()
+ + " identifier(s) deleted, allIdentifiersDeleted=" + allIdentifiersDeleted + "]");
+
+ final String name =
+ context.getString(R.string.device_unlock_notification_name);
+ final String title = context.getString(isFingerprint
+ ? R.string.fingerprint_dangling_notification_title
+ : R.string.face_dangling_notification_title);
+ final String content = isFingerprint
+ ? getFingerprintDanglingContentString(context, identifiers, allIdentifiersDeleted)
+ : context.getString(R.string.face_dangling_notification_msg);
+
+ // Create "Set up" notification action button.
+ final Intent setupIntent = new Intent(
+ isFingerprint ? BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH
+ : BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_LAUNCH);
+ final PendingIntent setupPendingIntent = PendingIntent.getBroadcastAsUser(context, 0,
+ setupIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+ final String setupText =
+ context.getString(R.string.biometric_dangling_notification_action_set_up);
+ final Notification.Action setupAction = new Notification.Action.Builder(
+ null, setupText, setupPendingIntent).build();
+
+ // Create "Not now" notification action button.
+ final Intent notNowIntent = new Intent(
+ isFingerprint ? BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_DISMISS
+ : BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_DISMISS);
+ final PendingIntent notNowPendingIntent = PendingIntent.getBroadcastAsUser(context, 0,
+ notNowIntent, PendingIntent.FLAG_IMMUTABLE, UserHandle.CURRENT);
+ final String notNowText = context.getString(
+ R.string.biometric_dangling_notification_action_not_now);
+ final Notification.Action notNowAction = new Notification.Action.Builder(
+ null, notNowText, notNowPendingIntent).build();
+
+ final String channel = isFingerprint ? FINGERPRINT_RE_ENROLL_CHANNEL
+ : FACE_RE_ENROLL_CHANNEL;
+ final String tag = isFingerprint ? FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG
+ : FACE_RE_ENROLL_NOTIFICATION_TAG;
+
+ showNotificationHelper(context, name, title, content, setupPendingIntent, setupAction,
+ notNowAction, Notification.CATEGORY_SYSTEM, channel, tag,
+ Notification.VISIBILITY_SECRET, false);
+ }
+
+ private static String getFingerprintDanglingContentString(Context context,
+ @NonNull List<String> fingerprints, boolean allFingerprintDeleted) {
+ if (fingerprints.isEmpty()) {
+ return null;
+ }
+
+ final int resId;
+ final int size = fingerprints.size();
+ final StringBuilder first = new StringBuilder();
+ final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
+ if (size > 1) {
+ // If there are more than 1 fingerprint deleted, the "second" will be the last
+ // fingerprint and set the others to "first".
+ // For example, if we have 3 fingerprints deleted(fp1, fp2 and fp3):
+ // first = "fp1, fp2"
+ // second = "fp3"
+ final String separator = ", ";
+ String second = null;
+ for (int i = 0; i < size; i++) {
+ if (i == size - 1) {
+ second = bidiFormatter.unicodeWrap("\"" + fingerprints.get(i) + "\"");
+ } else {
+ first.append(bidiFormatter.unicodeWrap("\""));
+ first.append(bidiFormatter.unicodeWrap(fingerprints.get(i)));
+ first.append(bidiFormatter.unicodeWrap("\""));
+ if (i < size - 2) {
+ first.append(bidiFormatter.unicodeWrap(separator));
+ }
+ }
+ }
+ if (allFingerprintDeleted) {
+ resId = R.string.fingerprint_dangling_notification_msg_all_deleted_2;
+ } else {
+ resId = R.string.fingerprint_dangling_notification_msg_2;
+ }
+
+ return String.format(context.getString(resId), first, second);
+ } else {
+ if (allFingerprintDeleted) {
+ resId = R.string.fingerprint_dangling_notification_msg_all_deleted_1;
+ } else {
+ resId = R.string.fingerprint_dangling_notification_msg_1;
+ }
+ first.append(bidiFormatter.unicodeWrap("\""));
+ first.append(bidiFormatter.unicodeWrap(fingerprints.get(0)));
+ first.append(bidiFormatter.unicodeWrap("\""));
+ return String.format(context.getString(resId), first);
+ }
+ }
+
private static void showNotificationHelper(Context context, String name, String title,
- String content, PendingIntent pendingIntent, String category,
- String channelName, String notificationTag, int visibility,
- boolean listenToDismissEvent) {
+ String content, PendingIntent pendingIntent, String category, String channelName,
+ String notificationTag, int visibility, boolean listenToDismissEvent) {
+ showNotificationHelper(context, name, title, content, pendingIntent,
+ null /* positiveAction */, null /* negativeAction */, category, channelName,
+ notificationTag, visibility, listenToDismissEvent);
+ }
+
+ private static void showNotificationHelper(Context context, String name, String title,
+ String content, PendingIntent pendingIntent, Notification.Action positiveAction,
+ Notification.Action negativeAction, String category, String channelName,
+ String notificationTag, int visibility, boolean listenToDismissEvent) {
Slog.v(TAG," listenToDismissEvent = " + listenToDismissEvent);
final PendingIntent dismissIntent = PendingIntent.getActivityAsUser(context,
0 /* requestCode */, DISMISS_FRR_INTENT, PendingIntent.FLAG_IMMUTABLE /* flags */,
@@ -202,6 +324,12 @@
.setContentIntent(pendingIntent)
.setVisibility(visibility);
+ if (positiveAction != null) {
+ builder.addAction(positiveAction);
+ }
+ if (negativeAction != null) {
+ builder.addAction(negativeAction);
+ }
if (listenToDismissEvent) {
builder.setDeleteIntent(dismissIntent);
}
@@ -253,4 +381,14 @@
UserHandle.CURRENT);
}
+ /**
+ * Cancels a fingerprint enrollment notification
+ */
+ public static void cancelFingerprintReEnrollNotification(@NonNull Context context) {
+ final NotificationManager notificationManager =
+ context.getSystemService(NotificationManager.class);
+ notificationManager.cancelAsUser(FINGERPRINT_RE_ENROLL_NOTIFICATION_TAG, NOTIFICATION_ID,
+ UserHandle.CURRENT);
+ }
+
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
index 6daaad1..81ab26d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/InternalEnumerateClient.java
@@ -22,6 +22,7 @@
import android.os.IBinder;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.BiometricsProto;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
@@ -44,6 +45,7 @@
private List<? extends BiometricAuthenticator.Identifier> mEnrolledList;
// List of templates to remove from the HAL
private List<BiometricAuthenticator.Identifier> mUnknownHALTemplates = new ArrayList<>();
+ private final int mInitialEnrolledSize;
protected InternalEnumerateClient(@NonNull Context context, @NonNull Supplier<T> lazyDaemon,
@NonNull IBinder token, int userId, @NonNull String owner,
@@ -55,6 +57,7 @@
super(context, lazyDaemon, token, null /* ClientMonitorCallbackConverter */, userId, owner,
0 /* cookie */, sensorId, logger, biometricContext);
mEnrolledList = enrolledList;
+ mInitialEnrolledSize = mEnrolledList.size();
mUtils = utils;
}
@@ -111,8 +114,10 @@
// At this point, mEnrolledList only contains templates known to the framework and
// not the HAL.
+ final List<String> names = new ArrayList<>();
for (int i = 0; i < mEnrolledList.size(); i++) {
BiometricAuthenticator.Identifier identifier = mEnrolledList.get(i);
+ names.add(identifier.getName().toString());
Slog.e(TAG, "doTemplateCleanup(): Removing dangling template from framework: "
+ identifier.getBiometricId() + " " + identifier.getName());
mUtils.removeBiometricForUser(getContext(),
@@ -120,6 +125,11 @@
getLogger().logUnknownEnrollmentInFramework();
}
+
+ // Send dangling notification.
+ if (!names.isEmpty()) {
+ sendDanglingNotification(names);
+ }
mEnrolledList.clear();
}
@@ -127,8 +137,24 @@
return mUnknownHALTemplates;
}
+ /**
+ * Send the dangling notification.
+ */
+ @VisibleForTesting
+ public void sendDanglingNotification(@NonNull List<String> identifierNames) {
+ if (!identifierNames.isEmpty()) {
+ Slog.e(TAG, "sendDanglingNotification(): initial enrolledSize="
+ + mInitialEnrolledSize + ", after clean up size=" + mEnrolledList.size());
+ final boolean allIdentifiersDeleted = mEnrolledList.size() == mInitialEnrolledSize;
+ BiometricNotificationUtils.showBiometricReEnrollNotification(
+ getContext(), identifierNames, allIdentifiersDeleted, getModality());
+ }
+ }
+
@Override
public int getProtoEnum() {
return BiometricsProto.CM_ENUMERATE;
}
+
+ protected abstract int getModality();
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java
index d85455e..6ce3bc5 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClient.java
@@ -18,12 +18,14 @@
import android.annotation.NonNull;
import android.content.Context;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.face.IFace;
import android.hardware.face.Face;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.BiometricUtils;
@@ -35,7 +37,8 @@
/**
* Face-specific internal enumerate client for the {@link IFace} AIDL HAL interface.
*/
-class FaceInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
+@VisibleForTesting
+public class FaceInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
private static final String TAG = "FaceInternalEnumerateClient";
FaceInternalEnumerateClient(@NonNull Context context,
@@ -56,4 +59,9 @@
mCallback.onClientFinished(this, false /* success */);
}
}
+
+ @Override
+ protected int getModality() {
+ return BiometricsProtoEnums.MODALITY_FACE;
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index e71cffe..f0a4189 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -52,6 +52,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.BiometricDanglingReceiver;
import com.android.server.biometrics.BiometricHandlerProvider;
import com.android.server.biometrics.Utils;
import com.android.server.biometrics.log.BiometricContext;
@@ -201,6 +202,7 @@
mBiometricHandlerProvider = biometricHandlerProvider;
initAuthenticationBroadcastReceiver();
+ initFaceDanglingBroadcastReceiver();
initSensors(resetLockoutRequiresChallenge, props);
}
@@ -214,6 +216,10 @@
});
}
+ private void initFaceDanglingBroadcastReceiver() {
+ new BiometricDanglingReceiver(mContext, BiometricsProtoEnums.MODALITY_FACE);
+ }
+
private void initSensors(boolean resetLockoutRequiresChallenge, SensorProps[] props) {
if (resetLockoutRequiresChallenge) {
Slog.d(getTag(), "Adding HIDL configs");
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java
index a5a832a..2849bd9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClient.java
@@ -18,11 +18,13 @@
import android.annotation.NonNull;
import android.content.Context;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.fingerprint.Fingerprint;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.BiometricUtils;
@@ -35,7 +37,8 @@
* Fingerprint-specific internal client supporting the
* {@link android.hardware.biometrics.fingerprint.IFingerprint} AIDL interface.
*/
-class FingerprintInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
+@VisibleForTesting
+public class FingerprintInternalEnumerateClient extends InternalEnumerateClient<AidlSession> {
private static final String TAG = "FingerprintInternalEnumerateClient";
protected FingerprintInternalEnumerateClient(@NonNull Context context,
@@ -56,4 +59,9 @@
mCallback.onClientFinished(this, false /* success */);
}
}
+
+ @Override
+ protected int getModality() {
+ return BiometricsProtoEnums.MODALITY_FINGERPRINT;
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
index 6874c71..c0dcd49 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java
@@ -58,6 +58,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.AuthenticationStatsBroadcastReceiver;
import com.android.server.biometrics.AuthenticationStatsCollector;
+import com.android.server.biometrics.BiometricDanglingReceiver;
import com.android.server.biometrics.BiometricHandlerProvider;
import com.android.server.biometrics.Flags;
import com.android.server.biometrics.Utils;
@@ -205,6 +206,7 @@
mBiometricHandlerProvider = biometricHandlerProvider;
initAuthenticationBroadcastReceiver();
+ initFingerprintDanglingBroadcastReceiver();
initSensors(resetLockoutRequiresHardwareAuthToken, props, gestureAvailabilityDispatcher);
}
@@ -218,6 +220,10 @@
});
}
+ private void initFingerprintDanglingBroadcastReceiver() {
+ new BiometricDanglingReceiver(mContext, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ }
+
private void initSensors(boolean resetLockoutRequiresHardwareAuthToken, SensorProps[] props,
GestureAvailabilityDispatcher gestureAvailabilityDispatcher) {
if (!resetLockoutRequiresHardwareAuthToken) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index d7a7dd4..70a1014 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1442,6 +1442,9 @@
// If there's an offload session, we need to set the initial doze brightness before
// the offload session starts controlling the brightness.
+ // During the transition DOZE_SUSPEND -> DOZE -> DOZE_SUSPEND, this brightness strategy
+ // will be selected again, meaning that no new brightness will be sent to the hardware and
+ // the display will stay at the brightness level set by the offload session.
if (Float.isNaN(brightnessState) && mFlags.isDisplayOffloadEnabled()
&& Display.isDozeState(state) && mDisplayOffloadSession != null) {
if (mAutomaticBrightnessController != null
@@ -1459,6 +1462,15 @@
if (BrightnessUtils.isValidBrightnessValue(rawBrightnessState)) {
brightnessState = clampScreenBrightness(rawBrightnessState);
mBrightnessReasonTemp.setReason(BrightnessReason.REASON_DOZE_INITIAL);
+
+ if (mAutomaticBrightnessController != null
+ && mAutomaticBrightnessStrategy.shouldUseAutoBrightness()) {
+ // Keep the brightness in the setting so that we can use it after the screen
+ // turns on, until a lux sample becomes available. We don't do this when
+ // auto-brightness is disabled - in that situation we still want to use
+ // the last brightness from when the screen was on.
+ updateScreenBrightnessSetting = currentBrightnessSetting != brightnessState;
+ }
}
}
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 4e9cf51..b47631c3 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -84,29 +84,6 @@
@NonNull IBinder toChannelToken);
/**
- * Sets the display id that the MouseCursorController will be forced to target. Pass
- * {@link android.view.Display#INVALID_DISPLAY} to clear the override.
- *
- * Note: This method generally blocks until the pointer display override has propagated.
- * When setting a new override, the caller should ensure that an input device that can control
- * the mouse pointer is connected. If a new override is set when no such input device is
- * connected, the caller may be blocked for an arbitrary period of time.
- *
- * @return true if the pointer displayId was set successfully, or false if it fails.
- *
- * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete.
- */
- public abstract boolean setVirtualMousePointerDisplayId(int pointerDisplayId);
-
- /**
- * Gets the display id that the MouseCursorController is being forced to target. Returns
- * {@link android.view.Display#INVALID_DISPLAY} if there is no override
- *
- * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete.
- */
- public abstract int getVirtualMousePointerDisplayId();
-
- /**
* Gets the current position of the mouse cursor.
*
* Returns NaN-s as the coordinates if the cursor is not available.
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index eb71952..308aed6 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -156,7 +156,6 @@
private static final int MSG_DELIVER_INPUT_DEVICES_CHANGED = 1;
private static final int MSG_RELOAD_DEVICE_ALIASES = 2;
private static final int MSG_DELIVER_TABLET_MODE_CHANGED = 3;
- private static final int MSG_POINTER_DISPLAY_ID_CHANGED = 4;
private static final int DEFAULT_VIBRATION_MAGNITUDE = 192;
private static final AdditionalDisplayInputProperties
@@ -282,33 +281,9 @@
// WARNING: Do not call other services outside of input while holding this lock.
private final Object mAdditionalDisplayInputPropertiesLock = new Object();
- // Forces the PointerController to target a specific display id.
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private int mOverriddenPointerDisplayId = Display.INVALID_DISPLAY;
-
- // PointerController is the source of truth of the pointer display. This is the value of the
- // latest pointer display id reported by PointerController.
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private int mAcknowledgedPointerDisplayId = Display.INVALID_DISPLAY;
- // This is the latest display id that IMS has requested PointerController to use. If there are
- // no devices that can control the pointer, PointerController may end up disregarding this
- // value.
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private int mRequestedPointerDisplayId = Display.INVALID_DISPLAY;
@GuardedBy("mAdditionalDisplayInputPropertiesLock")
private final SparseArray<AdditionalDisplayInputProperties> mAdditionalDisplayInputProperties =
new SparseArray<>();
- // This contains the per-display properties that are currently applied by native code. It should
- // be kept in sync with the properties for mRequestedPointerDisplayId.
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private final AdditionalDisplayInputProperties mCurrentDisplayProperties =
- new AdditionalDisplayInputProperties();
- // TODO(b/293587049): Pointer Icon Refactor: There can be more than one pointer icon
- // visible at once. Update this to support multi-pointer use cases.
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private int mPointerIconType = PointerIcon.TYPE_NOT_SPECIFIED;
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private PointerIcon mPointerIcon;
// Holds all the registered gesture monitors that are implemented as spy windows. The spy
// windows are mapped by their InputChannel tokens.
@@ -617,14 +592,9 @@
}
mNative.setDisplayViewports(vArray);
- // Attempt to update the pointer display when viewports change when there is no override.
+ // Attempt to update the default pointer display when the viewports change.
// Take care to not make calls to window manager while holding internal locks.
- final int pointerDisplayId = mWindowManagerCallbacks.getPointerDisplayId();
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- if (mOverriddenPointerDisplayId == Display.INVALID_DISPLAY) {
- updatePointerDisplayIdLocked(pointerDisplayId);
- }
- }
+ mNative.setPointerDisplayId(mWindowManagerCallbacks.getPointerDisplayId());
}
/**
@@ -1353,84 +1323,6 @@
properties -> properties.pointerIconVisible = visible);
}
- /**
- * Update the display on which the mouse pointer is shown.
- *
- * @return true if the pointer displayId changed, false otherwise.
- */
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private boolean updatePointerDisplayIdLocked(int pointerDisplayId) {
- if (mRequestedPointerDisplayId == pointerDisplayId) {
- return false;
- }
- mRequestedPointerDisplayId = pointerDisplayId;
- mNative.setPointerDisplayId(pointerDisplayId);
- applyAdditionalDisplayInputProperties();
- return true;
- }
-
- private void handlePointerDisplayIdChanged(PointerDisplayIdChangedArgs args) {
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- mAcknowledgedPointerDisplayId = args.mPointerDisplayId;
- // Notify waiting threads that the display of the mouse pointer has changed.
- mAdditionalDisplayInputPropertiesLock.notifyAll();
- }
- mWindowManagerCallbacks.notifyPointerDisplayIdChanged(
- args.mPointerDisplayId, args.mXPosition, args.mYPosition);
- }
-
- private boolean setVirtualMousePointerDisplayIdBlocking(int overrideDisplayId) {
- if (com.android.input.flags.Flags.enablePointerChoreographer()) {
- throw new IllegalStateException(
- "This must not be used when PointerChoreographer is enabled");
- }
- final boolean isRemovingOverride = overrideDisplayId == Display.INVALID_DISPLAY;
-
- // Take care to not make calls to window manager while holding internal locks.
- final int resolvedDisplayId = isRemovingOverride
- ? mWindowManagerCallbacks.getPointerDisplayId()
- : overrideDisplayId;
-
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- mOverriddenPointerDisplayId = overrideDisplayId;
-
- if (!updatePointerDisplayIdLocked(resolvedDisplayId)
- && mAcknowledgedPointerDisplayId == resolvedDisplayId) {
- // The requested pointer display is already set.
- return true;
- }
- if (isRemovingOverride && mAcknowledgedPointerDisplayId == Display.INVALID_DISPLAY) {
- // The pointer display override is being removed, but the current pointer display
- // is already invalid. This can happen when the PointerController is destroyed as a
- // result of the removal of all input devices that can control the pointer.
- return true;
- }
- try {
- // The pointer display changed, so wait until the change has propagated.
- mAdditionalDisplayInputPropertiesLock.wait(5_000 /*mills*/);
- } catch (InterruptedException ignored) {
- }
- // This request succeeds in two cases:
- // - This request was to remove the override, in which case the new pointer display
- // could be anything that WM has set.
- // - We are setting a new override, in which case the request only succeeds if the
- // reported new displayId is the one we requested. This check ensures that if two
- // competing overrides are requested in succession, the caller can be notified if one
- // of them fails.
- return isRemovingOverride || mAcknowledgedPointerDisplayId == overrideDisplayId;
- }
- }
-
- private int getVirtualMousePointerDisplayId() {
- if (com.android.input.flags.Flags.enablePointerChoreographer()) {
- throw new IllegalStateException(
- "This must not be used when PointerChoreographer is enabled");
- }
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- return mOverriddenPointerDisplayId;
- }
- }
-
private void setDisplayEligibilityForPointerCapture(int displayId, boolean isEligible) {
mNative.setDisplayEligibilityForPointerCapture(displayId, isEligible);
}
@@ -1714,45 +1606,10 @@
// Binder call
@Override
- public void setPointerIconType(int iconType) {
- if (iconType == PointerIcon.TYPE_CUSTOM) {
- throw new IllegalArgumentException("Use setCustomPointerIcon to set custom pointers");
- }
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- mPointerIcon = null;
- mPointerIconType = iconType;
-
- if (!mCurrentDisplayProperties.pointerIconVisible) return;
-
- mNative.setPointerIconType(mPointerIconType);
- }
- }
-
- // Binder call
- @Override
- public void setCustomPointerIcon(PointerIcon icon) {
- Objects.requireNonNull(icon);
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- mPointerIconType = PointerIcon.TYPE_CUSTOM;
- mPointerIcon = icon;
-
- if (!mCurrentDisplayProperties.pointerIconVisible) return;
-
- mNative.setCustomPointerIcon(mPointerIcon);
- }
- }
-
- // Binder call
- @Override
public boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId, int pointerId,
IBinder inputToken) {
Objects.requireNonNull(icon);
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- mPointerIconType = icon.getType();
- mPointerIcon = mPointerIconType == PointerIcon.TYPE_CUSTOM ? icon : null;
-
- return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken);
- }
+ return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken);
}
/**
@@ -2281,28 +2138,24 @@
private void dumpDisplayInputPropertiesValues(IndentingPrintWriter pw) {
synchronized (mAdditionalDisplayInputPropertiesLock) {
- if (mAdditionalDisplayInputProperties.size() != 0) {
- pw.println("mAdditionalDisplayInputProperties:");
- pw.increaseIndent();
+ pw.println("mAdditionalDisplayInputProperties:");
+ pw.increaseIndent();
+ try {
+ if (mAdditionalDisplayInputProperties.size() == 0) {
+ pw.println("<none>");
+ return;
+ }
for (int i = 0; i < mAdditionalDisplayInputProperties.size(); i++) {
- pw.println("displayId: "
- + mAdditionalDisplayInputProperties.keyAt(i));
+ pw.println("displayId: " + mAdditionalDisplayInputProperties.keyAt(i));
final AdditionalDisplayInputProperties properties =
mAdditionalDisplayInputProperties.valueAt(i);
pw.println("mousePointerAccelerationEnabled: "
+ properties.mousePointerAccelerationEnabled);
pw.println("pointerIconVisible: " + properties.pointerIconVisible);
}
+ } finally {
pw.decreaseIndent();
}
- if (mOverriddenPointerDisplayId != Display.INVALID_DISPLAY) {
- pw.println("mOverriddenPointerDisplayId: " + mOverriddenPointerDisplayId);
- }
-
- pw.println("mAcknowledgedPointerDisplayId=" + mAcknowledgedPointerDisplayId);
- pw.println("mRequestedPointerDisplayId=" + mRequestedPointerDisplayId);
- pw.println("mPointerIconType=" + PointerIcon.typeToString(mPointerIconType));
- pw.println("mPointerIcon=" + mPointerIcon);
}
}
private boolean checkCallingPermission(String permission, String func) {
@@ -2832,9 +2685,7 @@
@SuppressWarnings("unused")
@VisibleForTesting
void onPointerDisplayIdChanged(int pointerDisplayId, float xPosition, float yPosition) {
- mHandler.obtainMessage(MSG_POINTER_DISPLAY_ID_CHANGED,
- new PointerDisplayIdChangedArgs(pointerDisplayId, xPosition,
- yPosition)).sendToTarget();
+ // TODO(b/311416205): Remove.
}
@Override
@@ -2989,14 +2840,6 @@
*/
@Nullable
SurfaceControl createSurfaceForGestureMonitor(String name, int displayId);
-
- /**
- * Notify WindowManagerService when the display of the mouse pointer changes.
- * @param displayId The display on which the mouse pointer is shown.
- * @param x The x coordinate of the mouse pointer.
- * @param y The y coordinate of the mouse pointer.
- */
- void notifyPointerDisplayIdChanged(int displayId, float x, float y);
}
/**
@@ -3040,9 +2883,6 @@
boolean inTabletMode = (boolean) args.arg1;
deliverTabletModeChanged(whenNanos, inTabletMode);
break;
- case MSG_POINTER_DISPLAY_ID_CHANGED:
- handlePointerDisplayIdChanged((PointerDisplayIdChangedArgs) msg.obj);
- break;
}
}
}
@@ -3267,17 +3107,6 @@
}
@Override
- public boolean setVirtualMousePointerDisplayId(int pointerDisplayId) {
- return InputManagerService.this
- .setVirtualMousePointerDisplayIdBlocking(pointerDisplayId);
- }
-
- @Override
- public int getVirtualMousePointerDisplayId() {
- return InputManagerService.this.getVirtualMousePointerDisplayId();
- }
-
- @Override
public PointF getCursorPosition(int displayId) {
final float[] p = mNative.getMouseCursorPosition(displayId);
if (p == null || p.length != 2) {
@@ -3413,44 +3242,6 @@
}
}
- private void applyAdditionalDisplayInputProperties() {
- synchronized (mAdditionalDisplayInputPropertiesLock) {
- AdditionalDisplayInputProperties properties =
- mAdditionalDisplayInputProperties.get(mRequestedPointerDisplayId);
- if (properties == null) properties = DEFAULT_ADDITIONAL_DISPLAY_INPUT_PROPERTIES;
- applyAdditionalDisplayInputPropertiesLocked(properties);
- }
- }
-
- @GuardedBy("mAdditionalDisplayInputPropertiesLock")
- private void applyAdditionalDisplayInputPropertiesLocked(
- AdditionalDisplayInputProperties properties) {
- // Handle changes to each of the individual properties.
- // TODO(b/293587049): This approach for updating pointer display properties is only for when
- // PointerChoreographer is disabled. Remove this logic when PointerChoreographer is
- // permanently enabled.
-
- if (properties.pointerIconVisible != mCurrentDisplayProperties.pointerIconVisible) {
- mCurrentDisplayProperties.pointerIconVisible = properties.pointerIconVisible;
- if (properties.pointerIconVisible) {
- if (mPointerIconType == PointerIcon.TYPE_CUSTOM) {
- Objects.requireNonNull(mPointerIcon);
- mNative.setCustomPointerIcon(mPointerIcon);
- } else {
- mNative.setPointerIconType(mPointerIconType);
- }
- } else {
- mNative.setPointerIconType(PointerIcon.TYPE_NULL);
- }
- }
-
- if (properties.mousePointerAccelerationEnabled
- != mCurrentDisplayProperties.mousePointerAccelerationEnabled) {
- mCurrentDisplayProperties.mousePointerAccelerationEnabled =
- properties.mousePointerAccelerationEnabled;
- }
- }
-
private void updateAdditionalDisplayInputProperties(int displayId,
Consumer<AdditionalDisplayInputProperties> updater) {
synchronized (mAdditionalDisplayInputPropertiesLock) {
@@ -3473,13 +3264,6 @@
if (properties.allDefaults()) {
mAdditionalDisplayInputProperties.remove(displayId);
}
- if (displayId != mRequestedPointerDisplayId) {
- Log.i(TAG, "Not applying additional properties for display " + displayId
- + " because the pointer is currently targeting display "
- + mRequestedPointerDisplayId + ".");
- return;
- }
- applyAdditionalDisplayInputPropertiesLocked(properties);
}
}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 32d5044..f742360 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -189,12 +189,8 @@
void disableInputDevice(int deviceId);
- void setPointerIconType(int iconId);
-
void reloadPointerIcons();
- void setCustomPointerIcon(@NonNull PointerIcon icon);
-
boolean setPointerIcon(@NonNull PointerIcon icon, int displayId, int deviceId, int pointerId,
@NonNull IBinder inputToken);
@@ -467,15 +463,9 @@
public native void disableInputDevice(int deviceId);
@Override
- public native void setPointerIconType(int iconId);
-
- @Override
public native void reloadPointerIcons();
@Override
- public native void setCustomPointerIcon(PointerIcon icon);
-
- @Override
public native boolean setPointerIcon(PointerIcon icon, int displayId, int deviceId,
int pointerId, IBinder inputToken);
diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
index 7956e03..79f1a9c 100644
--- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
+++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java
@@ -330,14 +330,10 @@
mHandwritingSurface.startIntercepting(imePid, imeUid);
// Unset the pointer icon for the stylus in case the app had set it.
- if (com.android.input.flags.Flags.enablePointerChoreographer()) {
- Objects.requireNonNull(mContext.getSystemService(InputManager.class)).setPointerIcon(
- PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_NOT_SPECIFIED),
- downEvent.getDisplayId(), downEvent.getDeviceId(), downEvent.getPointerId(0),
- mHandwritingSurface.getInputChannel().getToken());
- } else {
- InputManagerGlobal.getInstance().setPointerIconType(PointerIcon.TYPE_NOT_SPECIFIED);
- }
+ Objects.requireNonNull(mContext.getSystemService(InputManager.class)).setPointerIcon(
+ PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_NOT_SPECIFIED),
+ downEvent.getDisplayId(), downEvent.getDeviceId(), downEvent.getPointerId(0),
+ mHandwritingSurface.getInputChannel().getToken());
return new HandwritingSession(mCurrentRequestId, mHandwritingSurface.getInputChannel(),
mHandwritingBuffer);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 0fde760..25e2e3a 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -2250,7 +2250,9 @@
// Check if the input method is changing.
// We expect the caller has already verified that the client is allowed to access this
// display ID.
- if (isSelectedMethodBoundLocked()) {
+ final String curId = bindingController.getCurId();
+ if (curId != null && curId.equals(bindingController.getSelectedMethodId())
+ && mDisplayIdToShowIme == mCurTokenDisplayId) {
if (cs.mCurSession != null) {
// Fast case: if we are already connected to the input method,
// then just return it.
@@ -2369,13 +2371,6 @@
}
@GuardedBy("ImfLock.class")
- private boolean isSelectedMethodBoundLocked() {
- String curId = getCurIdLocked();
- return curId != null && curId.equals(getSelectedMethodIdLocked())
- && mDisplayIdToShowIme == mCurTokenDisplayId;
- }
-
- @GuardedBy("ImfLock.class")
private void prepareClientSwitchLocked(ClientState cs) {
// If the client is changing, we need to switch over to the new
// one.
diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
index 0049213..d932bd4 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java
@@ -32,6 +32,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.os.Bundle;
import android.os.Environment;
import android.os.HandlerThread;
import android.os.LocaleList;
@@ -101,6 +102,11 @@
// the application setting the app-locale itself.
private final SharedPreferences mDelegateAppLocalePackages;
private final BroadcastReceiver mUserMonitor;
+ // To determine whether an app is pre-archived, check for Intent.EXTRA_ARCHIVAL upon receiving
+ // the initial PACKAGE_ADDED broadcast. If it is indeed pre-archived, perform the data
+ // restoration during the second PACKAGE_ADDED broadcast, which is sent subsequently when the
+ // app is installed.
+ private final Set<String> mPkgsToRestore;
LocaleManagerBackupHelper(LocaleManagerService localeManagerService,
PackageManager packageManager, HandlerThread broadcastHandlerThread) {
@@ -119,6 +125,7 @@
mStagedData = stagedData;
mDelegateAppLocalePackages = delegateAppLocalePackages != null ? delegateAppLocalePackages
: createPersistedInfo();
+ mPkgsToRestore = new ArraySet<>();
mUserMonitor = new UserMonitor();
IntentFilter filter = new IntentFilter();
@@ -251,6 +258,9 @@
LocalesInfo localesInfo = pkgStates.get(pkgName);
// Check if the application is already installed for the concerned user.
if (isPackageInstalledForUser(pkgName, userId)) {
+ if (mPkgsToRestore != null) {
+ mPkgsToRestore.remove(pkgName);
+ }
// Don't apply the restore if the locales have already been set for the app.
checkExistingLocalesAndApplyRestore(pkgName, localesInfo, userId);
} else {
@@ -279,23 +289,18 @@
/**
* <p><b>Note:</b> This is invoked by service's common monitor
- * {@link LocaleManagerServicePackageMonitor#onPackageAdded} when a new package is
+ * {@link LocaleManagerServicePackageMonitor#onPackageAddedWithExtras} when a new package is
* added on device.
*/
- void onPackageAdded(String packageName, int uid) {
- try {
- synchronized (mStagedDataLock) {
- cleanStagedDataForOldEntriesLocked();
-
- int userId = UserHandle.getUserId(uid);
- if (mStagedData.contains(userId)) {
- // Perform lazy restore only if the staged data exists.
- doLazyRestoreLocked(packageName, userId);
- }
+ void onPackageAddedWithExtras(String packageName, int uid, Bundle extras) {
+ boolean archived = false;
+ if (extras != null) {
+ archived = extras.getBoolean(Intent.EXTRA_ARCHIVAL, false);
+ if (archived && mPkgsToRestore != null) {
+ mPkgsToRestore.add(packageName);
}
- } catch (Exception e) {
- Slog.e(TAG, "Exception in onPackageAdded.", e);
}
+ checkStageDataAndApplyRestore(packageName, uid);
}
/**
@@ -305,6 +310,10 @@
*/
void onPackageUpdateFinished(String packageName, int uid) {
int userId = UserHandle.getUserId(uid);
+ if (mPkgsToRestore != null && mPkgsToRestore.contains(packageName)) {
+ mPkgsToRestore.remove(packageName);
+ checkStageDataAndApplyRestore(packageName, uid);
+ }
cleanApplicationLocalesIfNeeded(packageName, userId);
}
@@ -338,6 +347,25 @@
}
}
+ private void checkStageDataAndApplyRestore(String packageName, int uid) {
+ try {
+ synchronized (mStagedDataLock) {
+ cleanStagedDataForOldEntriesLocked();
+
+ int userId = UserHandle.getUserId(uid);
+ if (mStagedData.contains(userId)) {
+ if (mPkgsToRestore != null) {
+ mPkgsToRestore.remove(packageName);
+ }
+ // Perform lazy restore only if the staged data exists.
+ doLazyRestoreLocked(packageName, userId);
+ }
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Exception in onPackageAdded.", e);
+ }
+ }
+
private boolean isPackageInstalledForUser(String packageName, int userId) {
PackageInfo pkgInfo = null;
try {
diff --git a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
index ecd3614..e0a050f 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerServicePackageMonitor.java
@@ -17,6 +17,7 @@
package com.android.server.locales;
import android.annotation.NonNull;
+import android.os.Bundle;
import android.os.UserHandle;
import com.android.internal.content.PackageMonitor;
@@ -48,8 +49,8 @@
}
@Override
- public void onPackageAdded(String packageName, int uid) {
- mBackupHelper.onPackageAdded(packageName, uid);
+ public void onPackageAddedWithExtras(String packageName, int uid, Bundle extras) {
+ mBackupHelper.onPackageAddedWithExtras(packageName, uid, extras);
}
@Override
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index d6d134d..17f8abe 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -333,7 +333,12 @@
return false;
}
- if (didEventHappen(MESSAGE_DUPLICATION_PROBABILITY_PERCENT)) {
+ if (Flags.reliableMessageDuplicateDetectionService()
+ && didEventHappen(MESSAGE_DUPLICATION_PROBABILITY_PERCENT)) {
+ Log.i(TAG, "[TEST MODE] Duplicating message ("
+ + NUM_MESSAGES_TO_DUPLICATE
+ + " sends) with message sequence number: "
+ + message.getMessageSequenceNumber());
for (int i = 0; i < NUM_MESSAGES_TO_DUPLICATE; ++i) {
handleClientMessageCallback(contextHubId, hostEndpointId,
message, nanoappPermissions, messagePermissions);
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 869b89a..73647db 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -1305,22 +1305,20 @@
route.getId(),
requestId));
+ UserHandler userHandler = routerRecord.mUserRecord.mHandler;
if (managerRequestId != MediaRoute2ProviderService.REQUEST_ID_NONE) {
- ManagerRecord manager = routerRecord.mUserRecord.mHandler.findManagerWithId(
- toRequesterId(managerRequestId));
+ ManagerRecord manager = userHandler.findManagerWithId(toRequesterId(managerRequestId));
if (manager == null || manager.mLastSessionCreationRequest == null) {
Slog.w(TAG, "requestCreateSessionWithRouter2Locked: "
+ "Ignoring unknown request.");
- routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
- routerRecord, requestId);
+ userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
return;
}
if (!TextUtils.equals(manager.mLastSessionCreationRequest.mOldSession.getId(),
oldSession.getId())) {
Slog.w(TAG, "requestCreateSessionWithRouter2Locked: "
+ "Ignoring unmatched routing session.");
- routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
- routerRecord, requestId);
+ userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
return;
}
if (!TextUtils.equals(manager.mLastSessionCreationRequest.mRoute.getId(),
@@ -1333,29 +1331,28 @@
} else {
Slog.w(TAG, "requestCreateSessionWithRouter2Locked: "
+ "Ignoring unmatched route.");
- routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
- routerRecord, requestId);
+ userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
return;
}
}
manager.mLastSessionCreationRequest = null;
} else {
+ String defaultRouteId = userHandler.mSystemProvider.getDefaultRoute().getId();
if (route.isSystemRoute()
&& !routerRecord.hasSystemRoutingPermission()
- && !TextUtils.equals(route.getId(), MediaRoute2Info.ROUTE_ID_DEFAULT)) {
+ && !TextUtils.equals(route.getId(), defaultRouteId)) {
Slog.w(TAG, "MODIFY_AUDIO_ROUTING permission is required to transfer to"
+ route);
- routerRecord.mUserRecord.mHandler.notifySessionCreationFailedToRouter(
- routerRecord, requestId);
+ userHandler.notifySessionCreationFailedToRouter(routerRecord, requestId);
return;
}
}
long uniqueRequestId = toUniqueRequestId(routerRecord.mRouterId, requestId);
- routerRecord.mUserRecord.mHandler.sendMessage(
+ userHandler.sendMessage(
obtainMessage(
UserHandler::requestCreateSessionWithRouter2OnHandler,
- routerRecord.mUserRecord.mHandler,
+ userHandler,
uniqueRequestId,
managerRequestId,
transferInitiatorUserHandle,
@@ -1429,18 +1426,22 @@
"transferToRouteWithRouter2 | router: %s(id: %d), route: %s",
routerRecord.mPackageName, routerRecord.mRouterId, route.getId()));
+ UserHandler userHandler = routerRecord.mUserRecord.mHandler;
+ String defaultRouteId = userHandler.mSystemProvider.getDefaultRoute().getId();
if (route.isSystemRoute()
&& !routerRecord.hasSystemRoutingPermission()
- && !TextUtils.equals(route.getId(), MediaRoute2Info.ROUTE_ID_DEFAULT)) {
- routerRecord.mUserRecord.mHandler.sendMessage(
- obtainMessage(UserHandler::notifySessionCreationFailedToRouter,
- routerRecord.mUserRecord.mHandler,
- routerRecord, toOriginalRequestId(DUMMY_REQUEST_ID)));
+ && !TextUtils.equals(route.getId(), defaultRouteId)) {
+ userHandler.sendMessage(
+ obtainMessage(
+ UserHandler::notifySessionCreationFailedToRouter,
+ userHandler,
+ routerRecord,
+ toOriginalRequestId(DUMMY_REQUEST_ID)));
} else {
- routerRecord.mUserRecord.mHandler.sendMessage(
+ userHandler.sendMessage(
obtainMessage(
UserHandler::transferToRouteOnHandler,
- routerRecord.mUserRecord.mHandler,
+ userHandler,
DUMMY_REQUEST_ID,
transferInitiatorUserHandle,
routerRecord.mPackageName,
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
index c105b9c..6ce3ab4 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java
@@ -232,10 +232,16 @@
String sessionId,
String routeId,
@RoutingSessionInfo.TransferReason int transferReason) {
+ String selectedDeviceRouteId = mDeviceRouteController.getSelectedRoute().getId();
if (TextUtils.equals(routeId, MediaRoute2Info.ROUTE_ID_DEFAULT)) {
- // The currently selected route is the default route.
- Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
- return;
+ if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
+ // Transfer to the default route (which is the selected route). We replace the id to
+ // be the selected route id so that the transfer reason gets updated.
+ routeId = selectedDeviceRouteId;
+ } else {
+ Log.w(TAG, "Ignoring transfer to " + MediaRoute2Info.ROUTE_ID_DEFAULT);
+ return;
+ }
}
if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) {
@@ -250,11 +256,11 @@
}
}
- MediaRoute2Info selectedDeviceRoute = mDeviceRouteController.getSelectedRoute();
+ String finalRouteId = routeId; // Make a final copy to use it in the lambda.
boolean isAvailableDeviceRoute =
mDeviceRouteController.getAvailableRoutes().stream()
- .anyMatch(it -> it.getId().equals(routeId));
- boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRoute.getId());
+ .anyMatch(it -> it.getId().equals(finalRouteId));
+ boolean isSelectedDeviceRoute = TextUtils.equals(routeId, selectedDeviceRouteId);
if (isSelectedDeviceRoute || isAvailableDeviceRoute) {
// The requested route is managed by the device route controller. Note that the selected
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index c60f0af..209cbb7 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -46,6 +46,7 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApexStagedEvent;
+import android.content.pm.Flags;
import android.content.pm.IPackageManagerNative;
import android.content.pm.IStagedApexObserver;
import android.content.pm.PackageManager;
@@ -766,6 +767,10 @@
final PackageSetting ps = installRequest.getScannedPackageSetting();
final AndroidPackage pkg = ps.getPkg();
final boolean onIncremental = isIncrementalPath(ps.getPathString());
+ final boolean performDexOptForRollback = Flags.recoverabilityDetection()
+ ? !(installRequest.isRollback()
+ && installRequest.getInstallSource().mInitiatingPackageName.equals("android"))
+ : true;
return (!instantApp || Global.getInt(context.getContentResolver(),
Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
@@ -773,7 +778,8 @@
&& !pkg.isDebuggable()
&& (!onIncremental)
&& dexoptOptions.isCompilationEnabled()
- && !isApex;
+ && !isApex
+ && performDexOptForRollback;
}
private static class StagedApexObserver extends IStagedApexObserver.Stub {
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index 1793794..7a36f6d 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -35,6 +35,8 @@
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.role.RoleManager;
+import android.app.usage.StorageStats;
+import android.app.usage.StorageStatsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.IIntentReceiver;
@@ -136,6 +138,7 @@
import java.io.PrintWriter;
import java.net.URISyntaxException;
import java.security.SecureRandom;
+import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
@@ -275,6 +278,8 @@
return runClear();
case "get-archived-package-metadata":
return runGetArchivedPackageMetadata();
+ case "get-package-storage-stats":
+ return runGetPackageStorageStats();
case "install-archived":
return runArchivedInstall();
case "enable":
@@ -1861,6 +1866,103 @@
return 0;
}
+ /**
+ * Returns a string that shows the number of bytes in b, Kb, Mb or Gb.
+ */
+ protected static String getFormattedBytes(long size) {
+ double k = size/1024.0;
+ double m = size/1048576.0;
+ double g = size/1073741824.0;
+
+ DecimalFormat dec = new DecimalFormat("0.00");
+ if (g > 1) {
+ return dec.format(g).concat(" Gb");
+ } else if (m > 1) {
+ return dec.format(m).concat(" Mb");
+ } else if (k > 1) {
+ return dec.format(k).concat(" Kb");
+ }
+ return "";
+ }
+
+ /**
+ * Return the string that displays the data size.
+ */
+ private String getDataSizeDisplay(long size) {
+ String formattedOutput = getFormattedBytes(size);
+ if (!formattedOutput.isEmpty()) {
+ formattedOutput = " (" + formattedOutput + ")";
+ }
+ return Long.toString(size) + " bytes" + formattedOutput;
+ }
+
+ /**
+ * Display storage stats of the specified package.
+ *
+ * Usage: get-package-storage-stats [--usr USER_ID] PACKAGE
+ */
+ private int runGetPackageStorageStats() throws RemoteException {
+ final PrintWriter pw = getOutPrintWriter();
+ if (!android.content.pm.Flags.getPackageStorageStats()) {
+ pw.println("Error: get_package_storage_stats flag is not enabled");
+ return 1;
+ }
+ if (!android.app.usage.Flags.getAppBytesByDataTypeApi()) {
+ pw.println("Error: get_app_bytes_by_data_type_api flag is not enabled");
+ return 1;
+ }
+ int userId = UserHandle.USER_CURRENT;
+
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "--user":
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ break;
+ default:
+ pw.println("Error: Unknown option: " + opt);
+ return 1;
+ }
+ }
+
+ final String packageName = getNextArg();
+ if (packageName == null) {
+ pw.println("Error: package name not specified");
+ return 1;
+ }
+ try {
+ StorageStatsManager storageStatsManager =
+ mContext.getSystemService(StorageStatsManager.class);
+ final int translatedUserId = translateUserId(userId, UserHandle.USER_NULL,
+ "runGetPackageStorageStats");
+ StorageStats stats =
+ storageStatsManager.queryStatsForPackage(StorageManager.UUID_DEFAULT,
+ packageName, UserHandle.of(translatedUserId));
+
+ pw.println("code: " + getDataSizeDisplay(stats.getAppBytes()));
+ pw.println("data: " + getDataSizeDisplay(stats.getDataBytes()));
+ pw.println("cache: " + getDataSizeDisplay(stats.getCacheBytes()));
+ pw.println("apk: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+ StorageStats.APP_DATA_TYPE_FILE_TYPE_APK)));
+ pw.println("lib: " + getDataSizeDisplay(
+ stats.getAppBytesByDataType(StorageStats.APP_DATA_TYPE_LIB)));
+ pw.println("dm: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+ StorageStats.APP_DATA_TYPE_FILE_TYPE_DM)));
+ pw.println("dexopt artifacts: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+ StorageStats.APP_DATA_TYPE_FILE_TYPE_DEXOPT_ARTIFACT)));
+ pw.println("current profile : " + getDataSizeDisplay(stats.getAppBytesByDataType(
+ StorageStats.APP_DATA_TYPE_FILE_TYPE_CURRENT_PROFILE)));
+ pw.println("reference profile: " + getDataSizeDisplay(stats.getAppBytesByDataType(
+ StorageStats.APP_DATA_TYPE_FILE_TYPE_REFERENCE_PROFILE)));
+ pw.println("external cache: " + getDataSizeDisplay(stats.getExternalCacheBytes()));
+ } catch (Exception e) {
+ getErrPrintWriter().println("Failed to get storage stats, reason: " + e);
+ pw.println("Failure [failed to get storage stats], reason: " + e);
+ return -1;
+ }
+ return 0;
+ }
+
private int runInstallExisting() throws RemoteException {
final PrintWriter pw = getOutPrintWriter();
int userId = UserHandle.USER_CURRENT;
@@ -4869,6 +4971,8 @@
pw.println(" Displays the component name of the domain verification agent on device.");
pw.println(" If the component isn't enabled, an error message will be displayed.");
pw.println(" --user: return the agent of the given user (SYSTEM_USER if unspecified)");
+ pw.println(" get-package-storage-stats [--user <USER_ID>] <PACKAGE>");
+ pw.println(" Return the storage stats for the given app, if present");
pw.println("");
printArtServiceHelp();
pw.println("");
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 1309e44..41d6288 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -2139,10 +2139,17 @@
continue;
}
+ ComponentName unflattenOriginalComponentName = ComponentName.unflattenFromString(
+ originalComponentName);
+ if (unflattenOriginalComponentName == null) {
+ Slog.d(TAG, "Incorrect component name from the attributes");
+ continue;
+ }
+
activityInfos.add(
new ArchiveState.ArchiveActivityInfo(
title,
- ComponentName.unflattenFromString(originalComponentName),
+ unflattenOriginalComponentName,
iconPath,
monochromeIconPath));
}
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 4e02470..483d308 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -859,7 +859,6 @@
break;
case android.provider.Settings.System.SCREEN_BRIGHTNESS:
- case android.provider.Settings.System.SCREEN_BRIGHTNESS_FLOAT:
case android.provider.Settings.System.SCREEN_BRIGHTNESS_MODE:
if (callingUid == Process.SYSTEM_UID) {
return false;
diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java
index 1a9e012..f7eb29f 100644
--- a/services/core/java/com/android/server/pm/VerifyingSession.java
+++ b/services/core/java/com/android/server/pm/VerifyingSession.java
@@ -141,6 +141,8 @@
@NonNull
private final PackageManagerService mPm;
+ private final int mInstallReason;
+
VerifyingSession(UserHandle user, File stagedDir, IPackageInstallObserver2 observer,
PackageInstaller.SessionParams sessionParams, InstallSource installSource,
int installerUid, SigningDetails signingDetails, int sessionId, PackageLite lite,
@@ -168,6 +170,7 @@
mUserActionRequiredType = sessionParams.requireUserAction;
mIsInherit = sessionParams.mode == MODE_INHERIT_EXISTING;
mIsStaged = sessionParams.isStaged;
+ mInstallReason = sessionParams.installReason;
}
@Override
@@ -190,7 +193,9 @@
// Perform package verification and enable rollback (unless we are simply moving the
// package).
if (!mOriginInfo.mExisting) {
- if (!isApex() && !isArchivedInstallation()) {
+ final boolean verifyForRollback = Flags.recoverabilityDetection()
+ ? !isARollback() : true;
+ if (!isApex() && !isArchivedInstallation() && verifyForRollback) {
// TODO(b/182426975): treat APEX as APK when APK verification is concerned
sendApkVerificationRequest(pkgLite);
}
@@ -200,6 +205,11 @@
}
}
+ private boolean isARollback() {
+ return mInstallReason == PackageManager.INSTALL_REASON_ROLLBACK
+ && mInstallSource.mInitiatingPackageName.equals("android");
+ }
+
private void sendApkVerificationRequest(PackageInfoLite pkgLite) {
final int verificationId = mPm.mPendingVerificationToken++;
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 5e95a4b..202e94c6 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -7401,6 +7401,7 @@
}
static boolean isPip2ExperimentEnabled() {
- return Flags.enablePip2Implementation();
+ return Flags.enablePip2Implementation() || SystemProperties.getBoolean(
+ "wm_shell.pip2", false);
}
}
diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
index 25885ed..e8faff6 100644
--- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java
+++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java
@@ -96,6 +96,7 @@
interface TransactionReadyListener {
void onTransactionReady(int mSyncId, SurfaceControl.Transaction transaction);
default void onTransactionCommitTimeout() {}
+ default void onReadyTimeout() {}
}
/**
@@ -410,6 +411,7 @@
if (allFinished && !mReady) {
Slog.w(TAG, "Sync group " + mSyncId + " timed-out because not ready. If you see "
+ "this, please file a bug.");
+ mListener.onReadyTimeout();
}
finishNow();
removeFromDependencies(this);
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 0e446b8..eb1f3b4 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -39,6 +39,7 @@
import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel;
import static com.android.server.wm.PendingRemoteAnimationRegistry.TIMEOUT_MS;
import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskStackToFg;
+import static com.android.window.flags.Flags.balImprovedMetrics;
import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck;
import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator;
import static com.android.window.flags.Flags.balRequireOptInSameUid;
@@ -805,14 +806,25 @@
* or {@link #BAL_BLOCK} if the launch should be blocked
*/
BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
- int callingUid = state.mCallingUid;
- int callingPid = state.mCallingPid;
- final String callingPackage = state.mCallingPackage;
- WindowProcessController callerApp = state.mCallerApp;
+ // This is used to block background activity launch even if the app is still
+ // visible to user after user clicking home button.
+
+ // Normal apps with visible app window will be allowed to start activity if app switching
+ // is allowed, or apps like live wallpaper with non app visible window will be allowed.
+ final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW
+ || state.mAppSwitchState == APP_SWITCH_FG_ONLY;
+ if (appSwitchAllowedOrFg && state.mCallingUidHasAnyVisibleWindow) {
+ return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
+ /*background*/ false, "callingUid has visible window");
+ }
+ if (mService.mActiveUids.hasNonAppVisibleWindow(state.mCallingUid)) {
+ return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
+ /*background*/ false, "callingUid has non-app visible window");
+ }
// don't abort for the most important UIDs
- final int callingAppId = UserHandle.getAppId(callingUid);
- if (callingUid == Process.ROOT_UID
+ final int callingAppId = UserHandle.getAppId(state.mCallingUid);
+ if (state.mCallingUid == Process.ROOT_UID
|| callingAppId == Process.SYSTEM_UID
|| callingAppId == Process.NFC_UID) {
return new BalVerdict(
@@ -821,7 +833,7 @@
}
// Always allow home application to start activities.
- if (isHomeApp(callingUid, callingPackage)) {
+ if (isHomeApp(state.mCallingUid, state.mCallingPackage)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ false,
"Home app");
@@ -836,67 +848,46 @@
"Active ime");
}
- // This is used to block background activity launch even if the app is still
- // visible to user after user clicking home button.
- final int appSwitchState = mService.getBalAppSwitchesState();
-
- // don't abort if the callingUid has a visible window or is a persistent system process
- final int callingUidProcState = mService.mActiveUids.getUidState(callingUid);
- final boolean callingUidHasAnyVisibleWindow = mService.hasActiveVisibleWindow(callingUid);
- final boolean isCallingUidPersistentSystemProcess =
- callingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
-
- // Normal apps with visible app window will be allowed to start activity if app switching
- // is allowed, or apps like live wallpaper with non app visible window will be allowed.
- final boolean appSwitchAllowedOrFg =
- appSwitchState == APP_SWITCH_ALLOW || appSwitchState == APP_SWITCH_FG_ONLY;
- if (appSwitchAllowedOrFg && callingUidHasAnyVisibleWindow) {
- return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
- /*background*/ false, "callingUid has visible window");
- }
- if (mService.mActiveUids.hasNonAppVisibleWindow(callingUid)) {
- return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
- /*background*/ false, "callingUid has non-app visible window");
- }
-
- if (isCallingUidPersistentSystemProcess) {
+ // don't abort if the callingUid is a persistent system process
+ if (state.mIsCallingUidPersistentSystemProcess) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ false, "callingUid is persistent system process");
}
// don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission
- if (hasBalPermission(callingUid, callingPid)) {
+ if (hasBalPermission(state.mCallingUid, state.mCallingPid)) {
return new BalVerdict(BAL_ALLOW_PERMISSION,
/*background*/ true,
"START_ACTIVITIES_FROM_BACKGROUND permission granted");
}
// don't abort if the caller has the same uid as the recents component
- if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) {
+ if (mSupervisor.mRecentTasks.isCallerRecents(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Recents Component");
}
// don't abort if the callingUid is the device owner
- if (mService.isDeviceOwner(callingUid)) {
+ if (mService.isDeviceOwner(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Device Owner");
}
// don't abort if the callingUid is a affiliated profile owner
- if (mService.isAffiliatedProfileOwner(callingUid)) {
+ if (mService.isAffiliatedProfileOwner(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Affiliated Profile Owner");
}
// don't abort if the callingUid has companion device
- final int callingUserId = UserHandle.getUserId(callingUid);
- if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) {
+ final int callingUserId = UserHandle.getUserId(state.mCallingUid);
+ if (mService.isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Companion App");
}
// don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
- if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) {
+ if (mService.hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
+ state.mCallingPackage)) {
Slog.w(
TAG,
"Background activity start for "
- + callingPackage
+ + state.mCallingPackage
+ " allowed because SYSTEM_ALERT_WINDOW permission is granted.");
return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
/*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
@@ -905,7 +896,7 @@
// OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow(
AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
- callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED) {
+ state.mCallingUid, state.mCallingPackage) == AppOpsManager.MODE_ALLOWED) {
return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
"OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop is granted");
}
@@ -914,7 +905,7 @@
// That's the case for PendingIntent-based starts, since the creator's process might not be
// up and alive.
// Don't abort if the callerApp or other processes of that uid are allowed in any way.
- BalVerdict callerAppAllowsBal = checkProcessAllowsBal(callerApp, state);
+ BalVerdict callerAppAllowsBal = checkProcessAllowsBal(state.mCallerApp, state);
if (callerAppAllowsBal.allows()) {
return callerAppAllowsBal;
}
@@ -929,13 +920,6 @@
*/
BalVerdict checkBackgroundActivityStartAllowedBySender(BalState state) {
- if (state.isPendingIntentBalAllowedByPermission()
- && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) {
- return new BalVerdict(BAL_ALLOW_PERMISSION,
- /*background*/ false,
- "realCallingUid has BAL permission.");
- }
-
// Normal apps with visible app window will be allowed to start activity if app switching
// is allowed, or apps like live wallpaper with non app visible window will be allowed.
// The home app can start apps even if app switches are usually disallowed.
@@ -961,6 +945,13 @@
}
}
+ if (state.isPendingIntentBalAllowedByPermission()
+ && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) {
+ return new BalVerdict(BAL_ALLOW_PERMISSION,
+ /*background*/ false,
+ "realCallingUid has BAL permission.");
+ }
+
// if the realCallingUid is a persistent system process, abort if the IntentSender
// wasn't allowed to start an activity
if (state.mForcedBalByPiSender.allowsBackgroundActivityStarts()
@@ -1660,28 +1651,63 @@
(state.mOriginatingPendingIntent != null));
}
- @BalCode int code = finalVerdict.getCode();
- int callingUid = state.mCallingUid;
- int realCallingUid = state.mRealCallingUid;
- Intent intent = state.mIntent;
+ if (balImprovedMetrics()) {
+ if (shouldLogStats(finalVerdict, state)) {
+ String activityName;
+ if (shouldLogIntentActivity(finalVerdict, state)) {
+ Intent intent = state.mIntent;
+ activityName = intent == null ? "noIntent" // should never happen
+ : requireNonNull(intent.getComponent()).flattenToShortString();
+ } else {
+ activityName = "";
+ }
+ writeBalAllowedLog(activityName, finalVerdict.getCode(), state);
+ }
+ } else {
+ @BalCode int code = finalVerdict.getCode();
+ int callingUid = state.mCallingUid;
+ int realCallingUid = state.mRealCallingUid;
+ Intent intent = state.mIntent;
- if (code == BAL_ALLOW_PENDING_INTENT
- && (callingUid < Process.FIRST_APPLICATION_UID
- || realCallingUid < Process.FIRST_APPLICATION_UID)) {
- String activityName = intent != null
- ? requireNonNull(intent.getComponent()).flattenToShortString() : "";
- writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT,
- state);
- }
- if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND
- || code == BAL_ALLOW_SAW_PERMISSION) {
- // We don't need to know which activity in this case.
- writeBalAllowedLog("", code, state);
-
+ if (code == BAL_ALLOW_PENDING_INTENT
+ && (callingUid < Process.FIRST_APPLICATION_UID
+ || realCallingUid < Process.FIRST_APPLICATION_UID)) {
+ String activityName = intent != null
+ ? requireNonNull(intent.getComponent()).flattenToShortString() : "";
+ writeBalAllowedLog(activityName, BAL_ALLOW_PENDING_INTENT,
+ state);
+ }
+ if (code == BAL_ALLOW_PERMISSION || code == BAL_ALLOW_FOREGROUND
+ || code == BAL_ALLOW_SAW_PERMISSION) {
+ // We don't need to know which activity in this case.
+ writeBalAllowedLog("", code, state);
+ }
}
return finalVerdict;
}
+ @VisibleForTesting
+ boolean shouldLogStats(BalVerdict finalVerdict, BalState state) {
+ if (finalVerdict.blocks()) {
+ return false;
+ }
+ if (!state.isPendingIntent() && finalVerdict.getRawCode() == BAL_ALLOW_VISIBLE_WINDOW) {
+ return false;
+ }
+ if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()
+ && state.mResultForRealCaller.getRawCode() == BAL_ALLOW_VISIBLE_WINDOW) {
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ boolean shouldLogIntentActivity(BalVerdict finalVerdict, BalState state) {
+ return finalVerdict.mBasedOnRealCaller
+ ? state.mRealCallingUid < Process.FIRST_APPLICATION_UID
+ : state.mCallingUid < Process.FIRST_APPLICATION_UID;
+ }
+
@VisibleForTesting void writeBalAllowedLog(String activityName, int code, BalState state) {
FrameworkStatsLog.write(FrameworkStatsLog.BAL_ALLOWED,
activityName,
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4147249..c9a5e71 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -550,15 +550,6 @@
// TODO(multi-display): remove some of the usages.
boolean isDefaultDisplay;
- /** Detect user tapping outside of current focused task bounds .*/
- // TODO(b/315321016): Remove once pointer event detection is removed from WM.
- @VisibleForTesting
- final TaskTapPointerEventListener mTapDetector;
-
- /** Detect user tapping outside of current focused root task bounds .*/
- // TODO(b/315321016): Remove once pointer event detection is removed from WM.
- private Region mTouchExcludeRegion = new Region();
-
/** Save allocating when calculating rects */
private final Rect mTmpRect = new Rect();
private final Rect mTmpRect2 = new Rect();
@@ -571,10 +562,6 @@
final PinnedTaskController mPinnedTaskController;
- final ArrayList<WindowState> mTapExcludedWindows = new ArrayList<>();
- /** A collection of windows that provide tap exclude regions inside of them. */
- final ArraySet<WindowState> mTapExcludeProvidingWindows = new ArraySet<>();
-
private final LinkedList<ActivityRecord> mTmpUpdateAllDrawn = new LinkedList();
private final TaskForResizePointSearchResult mTmpTaskForResizePointSearchResult =
@@ -1193,18 +1180,6 @@
"PointerEventDispatcher" + mDisplayId, mDisplayId);
mPointerEventDispatcher = new PointerEventDispatcher(inputChannel);
- if (com.android.input.flags.Flags.removePointerEventTrackingInWm()) {
- mTapDetector = null;
- } else {
- // Tap Listeners are supported for:
- // 1. All physical displays (multi-display).
- // 2. VirtualDisplays on VR, AA (and everything else).
- mTapDetector = new TaskTapPointerEventListener(mWmService, this);
- registerPointerEventListener(mTapDetector);
- }
- if (mWmService.mMousePositionTracker != null) {
- registerPointerEventListener(mWmService.mMousePositionTracker);
- }
if (mWmService.mAtmService.getRecentTasks() != null) {
registerPointerEventListener(
mWmService.mAtmService.getRecentTasks().getInputListener());
@@ -3304,117 +3279,6 @@
mTmpTaskForResizePointSearchResult.process(taskDisplayArea, x, y, delta));
}
- void updateTouchExcludeRegion() {
- if (mTapDetector == null) {
- // The touch exclude region is used to detect the region outside of the focused task
- // so that the tap detector can detect outside touches. Don't calculate the exclude
- // region when the tap detector is disabled.
- return;
- }
- final Task focusedTask = (mFocusedApp != null ? mFocusedApp.getTask() : null);
- if (focusedTask == null) {
- mTouchExcludeRegion.setEmpty();
- } else {
- mTouchExcludeRegion.set(0, 0, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
- final int delta = dipToPixel(RESIZE_HANDLE_WIDTH_IN_DP, mDisplayMetrics);
- mTmpRect.setEmpty();
- mTmpRect2.setEmpty();
-
- forAllTasks(t -> { processTaskForTouchExcludeRegion(t, focusedTask, delta); });
-
- // If we removed the focused task above, add it back and only leave its
- // outside touch area in the exclusion. TapDetector is not interested in
- // any touch inside the focused task itself.
- if (!mTmpRect2.isEmpty()) {
- mTouchExcludeRegion.op(mTmpRect2, Region.Op.UNION);
- }
- }
- if (mInputMethodWindow != null && mInputMethodWindow.isVisible()) {
- // If the input method is visible and the user is typing, we don't want these touch
- // events to be intercepted and used to change focus. This would likely cause a
- // disappearance of the input method.
- mInputMethodWindow.getTouchableRegion(mTmpRegion);
- mTouchExcludeRegion.op(mTmpRegion, Op.UNION);
- }
- for (int i = mTapExcludedWindows.size() - 1; i >= 0; i--) {
- final WindowState win = mTapExcludedWindows.get(i);
- if (!win.isVisible()) {
- continue;
- }
- win.getTouchableRegion(mTmpRegion);
- mTouchExcludeRegion.op(mTmpRegion, Region.Op.UNION);
- }
- amendWindowTapExcludeRegion(mTouchExcludeRegion);
- mTapDetector.setTouchExcludeRegion(mTouchExcludeRegion);
- }
-
- private void processTaskForTouchExcludeRegion(Task task, Task focusedTask, int delta) {
- if (mTapDetector == null) {
- // The touch exclude region is used to detect the region outside of the focused task
- // so that the tap detector can detect outside touches. Don't calculate the exclude
- // region when the tap detector is disabled.
- }
- final ActivityRecord topVisibleActivity = task.getTopVisibleActivity();
-
- if (topVisibleActivity == null || !topVisibleActivity.hasContentToDisplay()) {
- return;
- }
-
- // Exclusion region is the region that TapDetector doesn't care about.
- // Here we want to remove all non-focused tasks from the exclusion region.
- // We also remove the outside touch area for resizing for all freeform
- // tasks (including the focused).
- // We save the focused task region once we find it, and add it back at the end.
- // If the task is root home task and it is resizable and visible (top of its root task),
- // we want to exclude the root docked task from touch so we need the entire screen area
- // and not just a small portion which the root home task currently is resized to.
- if (task.isActivityTypeHome() && task.isVisible() && task.isResizeable()) {
- task.getDisplayArea().getBounds(mTmpRect);
- } else {
- task.getDimBounds(mTmpRect);
- }
-
- if (task == focusedTask) {
- // Add the focused task rect back into the exclude region once we are done
- // processing root tasks.
- // NOTE: this *looks* like a no-op, but this usage of mTmpRect2 is expected by
- // updateTouchExcludeRegion.
- mTmpRect2.set(mTmpRect);
- }
-
- final boolean isFreeformed = task.inFreeformWindowingMode();
- if (task != focusedTask || isFreeformed) {
- if (isFreeformed) {
- // If the task is freeformed, enlarge the area to account for outside
- // touch area for resize.
- mTmpRect.inset(-delta, -delta);
- // Intersect with display content frame. If we have system decor (status bar/
- // navigation bar), we want to exclude that from the tap detection.
- // Otherwise, if the app is partially placed under some system button (eg.
- // Recents, Home), pressing that button would cause a full series of
- // unwanted transfer focus/resume/pause, before we could go home.
- mTmpRect.inset(getInsetsStateController().getRawInsetsState().calculateInsets(
- mTmpRect, systemBars() | ime(), false /* ignoreVisibility */));
- }
- mTouchExcludeRegion.op(mTmpRect, Region.Op.DIFFERENCE);
- }
- }
-
- /**
- * Union the region with all the tap exclude region provided by windows on this display.
- *
- * @param inOutRegion The region to be amended.
- */
- private void amendWindowTapExcludeRegion(Region inOutRegion) {
- final Region region = Region.obtain();
- for (int i = mTapExcludeProvidingWindows.size() - 1; i >= 0; i--) {
- final WindowState win = mTapExcludeProvidingWindows.valueAt(i);
- win.getTapExcludeRegion(region);
- inOutRegion.op(region, Op.UNION);
- }
- region.recycle();
- }
-
@Override
void switchUser(int userId) {
super.switchUser(userId);
@@ -3771,7 +3635,6 @@
pw.print("x"); pw.println(mDisplayInfo.largestNominalAppHeight);
pw.print(subPrefix + "deferred=" + mDeferredRemoval
+ " mLayoutNeeded=" + mLayoutNeeded);
- pw.println(" mTouchExcludeRegion=" + mTouchExcludeRegion);
pw.println();
super.dump(pw, prefix, dumpAll);
@@ -4120,7 +3983,6 @@
}
getInputMonitor().setFocusedAppLw(newFocus);
- updateTouchExcludeRegion();
return true;
}
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 8116f68..30f2d0d 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -21,13 +21,11 @@
import static android.view.View.DRAG_FLAG_GLOBAL_SAME_APPLICATION;
import static android.view.View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG;
-import static com.android.input.flags.Flags.enablePointerChoreographer;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import android.annotation.NonNull;
-import android.app.ActivityManager;
import android.content.ClipData;
import android.content.Context;
import android.hardware.input.InputManagerGlobal;
@@ -266,16 +264,12 @@
final SurfaceControl surfaceControl = mDragState.mSurfaceControl;
mDragState.broadcastDragStartedLocked(touchX, touchY);
- if (enablePointerChoreographer()) {
- if ((touchSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) {
- InputManagerGlobal.getInstance().setPointerIcon(
- PointerIcon.getSystemIcon(
- mService.mContext, PointerIcon.TYPE_GRABBING),
- mDragState.mDisplayContent.getDisplayId(), touchDeviceId,
- touchPointerId, mDragState.getInputToken());
- }
- } else {
- mDragState.overridePointerIconLocked(touchSource);
+ if ((touchSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) {
+ InputManagerGlobal.getInstance().setPointerIcon(
+ PointerIcon.getSystemIcon(
+ mService.mContext, PointerIcon.TYPE_GRABBING),
+ mDragState.mDisplayContent.getDisplayId(), touchDeviceId,
+ touchPointerId, mDragState.getInputToken());
}
// remember the thumb offsets for later
mDragState.mThumbOffsetX = thumbCenterX;
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index 5ed343a..72ae64c 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -45,7 +45,6 @@
import android.content.ClipDescription;
import android.graphics.Point;
import android.graphics.Rect;
-import android.hardware.input.InputManagerGlobal;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
@@ -58,9 +57,7 @@
import android.view.DragEvent;
import android.view.InputApplicationHandle;
import android.view.InputChannel;
-import android.view.InputDevice;
import android.view.InputWindowHandle;
-import android.view.PointerIcon;
import android.view.SurfaceControl;
import android.view.View;
import android.view.WindowManager;
@@ -110,7 +107,6 @@
boolean mCrossProfileCopyAllowed;
ClipData mData;
ClipDescription mDataDescription;
- int mTouchSource;
boolean mDragResult;
boolean mRelinquishDragSurfaceToDropTarget;
float mAnimatedScale = 1.0f;
@@ -263,12 +259,6 @@
Trace.instant(TRACE_TAG_WINDOW_MANAGER, "DragDropController#DRAG_ENDED");
}
- // Take the cursor back if it has been changed.
- if (isFromSource(InputDevice.SOURCE_MOUSE)) {
- mService.restorePointerIconLocked(mDisplayContent, mCurrentX, mCurrentY);
- mTouchSource = 0;
- }
-
// Clear the internal variables.
if (mInputSurface != null) {
mTransaction.remove(mInputSurface).apply();
@@ -762,18 +752,6 @@
return animator;
}
- private boolean isFromSource(int source) {
- return (mTouchSource & source) == source;
- }
-
- void overridePointerIconLocked(int touchSource) {
- mTouchSource = touchSource;
- if (isFromSource(InputDevice.SOURCE_MOUSE)) {
- // TODO(b/293587049): Pointer Icon Refactor: Set the pointer icon from the drag window.
- InputManagerGlobal.getInstance().setPointerIconType(PointerIcon.TYPE_GRABBING);
- }
- }
-
private class AnimationListener
implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {
@Override
diff --git a/services/core/java/com/android/server/wm/InputConfigAdapter.java b/services/core/java/com/android/server/wm/InputConfigAdapter.java
index 119fafd..ae6e724 100644
--- a/services/core/java/com/android/server/wm/InputConfigAdapter.java
+++ b/services/core/java/com/android/server/wm/InputConfigAdapter.java
@@ -20,8 +20,6 @@
import android.view.InputWindowHandle.InputConfigFlags;
import android.view.WindowManager.LayoutParams;
-import java.util.List;
-
/**
* A helper to determine the {@link InputConfigFlags} that control the behavior of an input window
* from several WM attributes.
@@ -47,7 +45,7 @@
* input configurations that can be mapped directly from a corresponding LayoutParams input
* feature.
*/
- private static final List<FlagMapping> INPUT_FEATURE_TO_CONFIG_MAP = List.of(
+ private static final FlagMapping[] INPUT_FEATURE_TO_CONFIG_MAP = {
new FlagMapping(
LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL,
InputConfig.NO_INPUT_CHANNEL, false /* inverted */),
@@ -59,7 +57,8 @@
InputConfig.SPY, false /* inverted */),
new FlagMapping(
LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY,
- InputConfig.SENSITIVE_FOR_PRIVACY, false /* inverted */));
+ InputConfig.SENSITIVE_FOR_PRIVACY, false /* inverted */)
+ };
@InputConfigFlags
private static final int INPUT_FEATURE_TO_CONFIG_MASK =
@@ -72,7 +71,7 @@
* NOTE: The layout params flag {@link LayoutParams#FLAG_NOT_FOCUSABLE} is not handled by this
* adapter, and must be handled explicitly.
*/
- private static final List<FlagMapping> LAYOUT_PARAM_FLAG_TO_CONFIG_MAP = List.of(
+ private static final FlagMapping[] LAYOUT_PARAM_FLAG_TO_CONFIG_MAP = {
new FlagMapping(
LayoutParams.FLAG_NOT_TOUCHABLE,
InputConfig.NOT_TOUCHABLE, false /* inverted */),
@@ -84,7 +83,8 @@
InputConfig.WATCH_OUTSIDE_TOUCH, false /* inverted */),
new FlagMapping(
LayoutParams.FLAG_SLIPPERY,
- InputConfig.SLIPPERY, false /* inverted */));
+ InputConfig.SLIPPERY, false /* inverted */)
+ };
@InputConfigFlags
private static final int LAYOUT_PARAM_FLAG_TO_CONFIG_MASK =
@@ -119,7 +119,7 @@
}
@InputConfigFlags
- private static int applyMapping(int flags, List<FlagMapping> flagToConfigMap) {
+ private static int applyMapping(int flags, FlagMapping[] flagToConfigMap) {
int inputConfig = 0;
for (final FlagMapping mapping : flagToConfigMap) {
final boolean flagSet = (flags & mapping.mFlag) != 0;
@@ -131,7 +131,7 @@
}
@InputConfigFlags
- private static int computeMask(List<FlagMapping> flagToConfigMap) {
+ private static int computeMask(FlagMapping[] flagToConfigMap) {
int mask = 0;
for (final FlagMapping mapping : flagToConfigMap) {
mask |= mapping.mInputConfig;
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index a84ebd9..22ca82a 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -290,22 +290,6 @@
}
}
- @Override
- public void notifyPointerDisplayIdChanged(int displayId, float x, float y) {
- synchronized (mService.mGlobalLock) {
- mService.setMousePointerDisplayId(displayId);
- if (displayId == Display.INVALID_DISPLAY) return;
-
- final DisplayContent dc = mService.mRoot.getDisplayContent(displayId);
- if (dc == null) {
- Slog.wtf(TAG, "The mouse pointer was moved to display " + displayId
- + " that does not have a valid DisplayContent.");
- return;
- }
- mService.restorePointerIconLocked(dc, x, y);
- }
- }
-
/** Waits until the built-in input devices have been configured. */
public boolean waitForInputDevicesReady(long timeoutMillis) {
synchronized (mInputDevicesReadyMonitor) {
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 6003c1b..be8c2ae 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -911,7 +911,6 @@
dc.getInputMonitor().updateInputWindowsLw(true /*force*/);
dc.updateSystemGestureExclusion();
dc.updateKeepClearAreas();
- dc.updateTouchExcludeRegion();
});
// Check to see if we are now in a state where the screen should
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index bb86460..3b3eeb4 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -740,16 +740,6 @@
}
@Override
- public void updatePointerIcon(IWindow window) {
- final long identity = Binder.clearCallingIdentity();
- try {
- mService.updatePointerIcon(window);
- } finally {
- Binder.restoreCallingIdentity(identity);
- }
- }
-
- @Override
public void updateTapExcludeRegion(IWindow window, Region region) {
final long identity = Binder.clearCallingIdentity();
try {
diff --git a/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java b/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
deleted file mode 100644
index ac244c7..0000000
--- a/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2013 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.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
-import static android.view.PointerIcon.TYPE_NOT_SPECIFIED;
-import static android.view.PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
-import static android.view.PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
-import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
-
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.hardware.input.InputManagerGlobal;
-import android.view.InputDevice;
-import android.view.MotionEvent;
-import android.view.WindowManagerPolicyConstants.PointerEventListener;
-
-import com.android.server.wm.WindowManagerService.H;
-
-/**
- * 1. Adjust the top most focus display if touch down on some display.
- * 2. Adjust the pointer icon when cursor moves to the task bounds.
- */
-public class TaskTapPointerEventListener implements PointerEventListener {
-
- private final Region mTouchExcludeRegion = new Region();
- private final WindowManagerService mService;
- private final DisplayContent mDisplayContent;
- private final Rect mTmpRect = new Rect();
- private int mPointerIconType = TYPE_NOT_SPECIFIED;
-
- public TaskTapPointerEventListener(WindowManagerService service,
- DisplayContent displayContent) {
- // TODO(b/315321016): Remove this class when the flag rollout is complete.
- if (com.android.input.flags.Flags.removePointerEventTrackingInWm()) {
- throw new IllegalStateException("TaskTapPointerEventListener should not be used!");
- }
- mService = service;
- mDisplayContent = displayContent;
- }
-
- private void restorePointerIcon(int x, int y) {
- if (mPointerIconType != TYPE_NOT_SPECIFIED) {
- mPointerIconType = TYPE_NOT_SPECIFIED;
- // Find the underlying window and ask it to restore the pointer icon.
- mService.mH.removeMessages(H.RESTORE_POINTER_ICON);
- mService.mH.obtainMessage(H.RESTORE_POINTER_ICON,
- x, y, mDisplayContent).sendToTarget();
- }
- }
-
- @Override
- public void onPointerEvent(MotionEvent motionEvent) {
- switch (motionEvent.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- final int x;
- final int y;
- if (motionEvent.getSource() == InputDevice.SOURCE_MOUSE) {
- x = (int) motionEvent.getXCursorPosition();
- y = (int) motionEvent.getYCursorPosition();
- } else {
- x = (int) motionEvent.getX();
- y = (int) motionEvent.getY();
- }
-
- synchronized (this) {
- if (!mTouchExcludeRegion.contains(x, y)) {
- mService.mTaskPositioningController.handleTapOutsideTask(
- mDisplayContent, x, y);
- }
- }
- }
- break;
- case MotionEvent.ACTION_HOVER_ENTER:
- case MotionEvent.ACTION_HOVER_MOVE: {
- final int x = (int) motionEvent.getX();
- final int y = (int) motionEvent.getY();
- if (mTouchExcludeRegion.contains(x, y)) {
- restorePointerIcon(x, y);
- break;
- }
- final Task task = mDisplayContent.findTaskForResizePoint(x, y);
- int iconType = TYPE_NOT_SPECIFIED;
- if (task != null) {
- task.getDimBounds(mTmpRect);
- if (!mTmpRect.isEmpty() && !mTmpRect.contains(x, y)) {
- if (x < mTmpRect.left) {
- iconType =
- (y < mTmpRect.top) ? TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW :
- (y > mTmpRect.bottom) ? TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW :
- TYPE_HORIZONTAL_DOUBLE_ARROW;
- } else if (x > mTmpRect.right) {
- iconType =
- (y < mTmpRect.top) ? TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW :
- (y > mTmpRect.bottom) ? TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW :
- TYPE_HORIZONTAL_DOUBLE_ARROW;
- } else if (y < mTmpRect.top || y > mTmpRect.bottom) {
- iconType = TYPE_VERTICAL_DOUBLE_ARROW;
- }
- }
- }
- if (mPointerIconType != iconType) {
- mPointerIconType = iconType;
- if (mPointerIconType == TYPE_NOT_SPECIFIED) {
- // Find the underlying window and ask it restore the pointer icon.
- mService.mH.removeMessages(H.RESTORE_POINTER_ICON);
- mService.mH.obtainMessage(H.RESTORE_POINTER_ICON,
- x, y, mDisplayContent).sendToTarget();
- } else {
- InputManagerGlobal.getInstance()
- .setPointerIconType(mPointerIconType);
- }
- }
- }
- break;
- case MotionEvent.ACTION_HOVER_EXIT: {
- final int x = (int) motionEvent.getX();
- final int y = (int) motionEvent.getY();
- restorePointerIcon(x, y);
- }
- break;
- }
- }
-
- void setTouchExcludeRegion(Region newRegion) {
- synchronized (this) {
- mTouchExcludeRegion.set(newRegion);
- }
- }
-}
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 1543263..7ec31d5 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -1648,14 +1648,6 @@
}
if (mController.useFullReadyTracking()) {
- if (mReadyTracker.mMet.isEmpty()) {
- Slog.e(TAG, "#" + mSyncId + ": No conditions provided");
- } else {
- for (int i = 0; i < mReadyTracker.mConditions.size(); ++i) {
- Slog.e(TAG, "#" + mSyncId + ": unmet condition at ready: "
- + mReadyTracker.mConditions.get(i));
- }
- }
for (int i = 0; i < mReadyTracker.mMet.size(); ++i) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "#%d: Met condition: %s",
mSyncId, mReadyTracker.mMet.get(i));
@@ -3360,6 +3352,18 @@
applyReady();
}
+ @Override
+ public void onReadyTimeout() {
+ if (!mController.useFullReadyTracking()) {
+ Slog.e(TAG, "#" + mSyncId + " readiness timeout, used=" + mReadyTrackerOld.mUsed
+ + " deferReadyDepth=" + mReadyTrackerOld.mDeferReadyDepth
+ + " group=" + mReadyTrackerOld.mReadyGroups);
+ return;
+ }
+ Slog.e(TAG, "#" + mSyncId + " met conditions: " + mReadyTracker.mMet);
+ Slog.e(TAG, "#" + mSyncId + " unmet conditions: " + mReadyTracker.mConditions);
+ }
+
/**
* Represents a condition that must be met before an associated transition can be considered
* ready.
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index dbe3d36..e02e5be 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -81,15 +81,11 @@
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
-import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
-import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
import static android.view.WindowManager.LayoutParams.TYPE_QS_DIALOG;
-import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
-import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED;
import static android.view.WindowManager.TRANSIT_NONE;
@@ -203,7 +199,6 @@
import android.hardware.configstore.V1_1.ISurfaceFlingerConfigs;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerInternal;
-import android.hardware.input.InputManager;
import android.hardware.input.InputSettings;
import android.net.Uri;
import android.os.Binder;
@@ -289,8 +284,6 @@
import android.view.InsetsState;
import android.view.KeyEvent;
import android.view.MagnificationSpec;
-import android.view.MotionEvent;
-import android.view.PointerIcon;
import android.view.RemoteAnimationAdapter;
import android.view.ScrollCaptureResponse;
import android.view.Surface;
@@ -335,6 +328,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
+import com.android.internal.os.TransferPipe;
import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.internal.policy.IKeyguardLockedStateListener;
import com.android.internal.policy.IShortcutService;
@@ -545,13 +539,16 @@
if (asProto) {
return;
}
+
+ final long timeoutMs = 1000L;
mAtmService.dumpActivity(fd, pw, /* name= */ "all", /* args= */ new String[]{},
/* opti= */ 0,
/* dumpAll= */ true,
/* dumpVisibleRootTasksOnly= */ true,
/* dumpFocusedRootTaskOnly= */ false, INVALID_DISPLAY, UserHandle.USER_ALL,
- /* timeout= */ 1000
+ timeoutMs
);
+ dumpVisibleWindowClients(fd, pw, timeoutMs);
}
@Override
@@ -1523,18 +1520,6 @@
}
}
- static boolean excludeWindowTypeFromTapOutTask(int windowType) {
- switch (windowType) {
- case TYPE_STATUS_BAR:
- case TYPE_NOTIFICATION_SHADE:
- case TYPE_NAVIGATION_BAR:
- case TYPE_INPUT_METHOD_DIALOG:
- case TYPE_VOLUME_OVERLAY:
- return true;
- }
- return false;
- }
-
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, @InsetsType int requestedVisibleTypes,
InputChannel outInputChannel, InsetsState outInsetsState,
@@ -1833,10 +1818,6 @@
displayContent.mWinAddedSinceNullFocus.add(win);
}
- if (excludeWindowTypeFromTapOutTask(type)) {
- displayContent.mTapExcludedWindows.add(win);
- }
-
win.mSession.onWindowAdded(win);
mWindowMap.put(client.asBinder(), win);
win.initAppOpsState();
@@ -5716,7 +5697,6 @@
public static final int UPDATE_ANIMATION_SCALE = 51;
public static final int WINDOW_HIDE_TIMEOUT = 52;
- public static final int RESTORE_POINTER_ICON = 55;
public static final int SET_HAS_OVERLAY_UI = 58;
public static final int ANIMATION_FAILSAFE = 60;
public static final int RECOMPUTE_FOCUS = 61;
@@ -5949,12 +5929,6 @@
}
break;
}
- case RESTORE_POINTER_ICON: {
- synchronized (mGlobalLock) {
- restorePointerIconLocked((DisplayContent)msg.obj, msg.arg1, msg.arg2);
- }
- break;
- }
case SET_HAS_OVERLAY_UI: {
mAmInternal.setHasOverlayUi(msg.arg1, msg.arg2 == 1);
break;
@@ -7574,144 +7548,6 @@
}
}
- // The mouse position tracker will be obsolete after the Pointer Icon Refactor.
- // TODO(b/293587049): Remove after the refactoring is fully rolled out.
- @Nullable
- final MousePositionTracker mMousePositionTracker =
- com.android.input.flags.Flags.enablePointerChoreographer() ? null
- : new MousePositionTracker();
-
- private static class MousePositionTracker implements PointerEventListener {
- private boolean mLatestEventWasMouse;
- private float mLatestMouseX;
- private float mLatestMouseY;
-
- /**
- * The display that the pointer (mouse cursor) is currently shown on. This is updated
- * directly by InputManagerService when the pointer display changes.
- */
- private int mPointerDisplayId = INVALID_DISPLAY;
-
- /**
- * Update the mouse cursor position as a result of a mouse movement.
- * @return true if the position was successfully updated, false otherwise.
- */
- boolean updatePosition(int displayId, float x, float y) {
- synchronized (this) {
- mLatestEventWasMouse = true;
-
- if (displayId != mPointerDisplayId) {
- // The display of the position update does not match the display on which the
- // mouse pointer is shown, so do not update the position.
- return false;
- }
- mLatestMouseX = x;
- mLatestMouseY = y;
- return true;
- }
- }
-
- void setPointerDisplayId(int displayId) {
- synchronized (this) {
- mPointerDisplayId = displayId;
- }
- }
-
- @Override
- public void onPointerEvent(MotionEvent motionEvent) {
- if (motionEvent.isFromSource(InputDevice.SOURCE_MOUSE)) {
- updatePosition(motionEvent.getDisplayId(), motionEvent.getRawX(),
- motionEvent.getRawY());
- } else {
- synchronized (this) {
- mLatestEventWasMouse = false;
- }
- }
- }
- };
-
- void updatePointerIcon(IWindow client) {
- if (mMousePositionTracker == null) {
- return;
- }
- int pointerDisplayId;
- float mouseX, mouseY;
-
- synchronized(mMousePositionTracker) {
- if (!mMousePositionTracker.mLatestEventWasMouse) {
- return;
- }
- mouseX = mMousePositionTracker.mLatestMouseX;
- mouseY = mMousePositionTracker.mLatestMouseY;
- pointerDisplayId = mMousePositionTracker.mPointerDisplayId;
- }
-
- synchronized (mGlobalLock) {
- if (mDragDropController.dragDropActiveLocked()) {
- // Drag cursor overrides the app cursor.
- return;
- }
- WindowState callingWin = windowForClientLocked(null, client, false);
- if (callingWin == null) {
- ProtoLog.w(WM_ERROR, "Bad requesting window %s", client);
- return;
- }
- final DisplayContent displayContent = callingWin.getDisplayContent();
- if (displayContent == null) {
- return;
- }
- if (pointerDisplayId != displayContent.getDisplayId()) {
- // Do not let the pointer icon be updated by a window on a different display.
- return;
- }
- WindowState windowUnderPointer =
- displayContent.getTouchableWinAtPointLocked(mouseX, mouseY);
- if (windowUnderPointer != callingWin) {
- return;
- }
- try {
- windowUnderPointer.mClient.updatePointerIcon(
- windowUnderPointer.translateToWindowX(mouseX),
- windowUnderPointer.translateToWindowY(mouseY));
- } catch (RemoteException e) {
- ProtoLog.w(WM_ERROR, "unable to update pointer icon");
- }
- }
- }
-
- void restorePointerIconLocked(DisplayContent displayContent, float latestX, float latestY) {
- if (mMousePositionTracker == null) {
- return;
- }
- // Mouse position tracker has not been getting updates while dragging, update it now.
- if (!mMousePositionTracker.updatePosition(
- displayContent.getDisplayId(), latestX, latestY)) {
- // The mouse position could not be updated, so ignore this request.
- return;
- }
-
- WindowState windowUnderPointer =
- displayContent.getTouchableWinAtPointLocked(latestX, latestY);
- if (windowUnderPointer != null) {
- try {
- windowUnderPointer.mClient.updatePointerIcon(
- windowUnderPointer.translateToWindowX(latestX),
- windowUnderPointer.translateToWindowY(latestY));
- } catch (RemoteException e) {
- ProtoLog.w(WM_ERROR, "unable to restore pointer icon");
- }
- } else {
- mContext.getSystemService(InputManager.class)
- .setPointerIconType(PointerIcon.TYPE_DEFAULT);
- }
- }
- void setMousePointerDisplayId(int displayId) {
- if (mMousePositionTracker == null) {
- return;
- }
- mMousePositionTracker.setPointerDisplayId(displayId);
- }
-
/**
* Update a tap exclude region in the window identified by the provided id. Touches down on this
* region will not:
@@ -10350,4 +10186,32 @@
}
return true;
}
+
+ /**
+ * Dump ViewRootImpl for visible non-activity windows.
+ */
+ private void dumpVisibleWindowClients(FileDescriptor fd, PrintWriter pw, long timeout) {
+ final ArrayList<WindowState> systemWindows = new ArrayList<>();
+ synchronized (mGlobalLock) {
+ mRoot.forAllWindows(w -> {
+ if (!w.isActivityWindow() && w.isVisibleNow()) {
+ systemWindows.add(w);
+ }
+ }, false /* traverseTopToBottom */);
+ }
+
+ systemWindows.forEach(w -> {
+ pw.println("---------------------------------");
+ pw.println(w.toString());
+ pw.flush();
+ try (TransferPipe tp = new TransferPipe()) {
+ w.mClient.dumpWindow(tp.getWriteFd());
+ tp.go(fd, timeout);
+ } catch (IOException e) {
+ pw.println("Failure while dumping the window: " + e);
+ } catch (RemoteException e) {
+ pw.println("Got a RemoteException while dumping the window");
+ }
+ });
+ }
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index e90c845..dddc7b1 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -2359,18 +2359,12 @@
}
final int type = mAttrs.type;
- if (WindowManagerService.excludeWindowTypeFromTapOutTask(type)) {
- dc.mTapExcludedWindows.remove(this);
- }
if (type == TYPE_PRESENTATION || type == TYPE_PRIVATE_PRESENTATION) {
mWmService.mDisplayManagerInternal.onPresentation(dc.getDisplay().getDisplayId(),
/*isShown=*/ false);
}
- // Remove this window from mTapExcludeProvidingWindows. If it was not registered, this will
- // not do anything.
- dc.mTapExcludeProvidingWindows.remove(this);
dc.getDisplayPolicy().removeWindowLw(this);
disposeInputChannel();
@@ -5526,18 +5520,10 @@
// Clear the tap excluded region if the region passed in is null or empty.
if (region == null || region.isEmpty()) {
mTapExcludeRegion.setEmpty();
- // Remove this window from mTapExcludeProvidingWindows since it won't be providing
- // tap exclude regions.
- currentDisplay.mTapExcludeProvidingWindows.remove(this);
} else {
mTapExcludeRegion.set(region);
- // Make sure that this window is registered as one that provides a tap exclude region
- // for its containing display.
- currentDisplay.mTapExcludeProvidingWindows.add(this);
}
- // Trigger touch exclude region update on current display.
- currentDisplay.updateTouchExcludeRegion();
// Trigger touchable region update for this window.
currentDisplay.getInputMonitor().updateInputWindowsLw(true /* force */);
}
@@ -6074,6 +6060,10 @@
return mPrepareSyncSeqId > 0;
}
+ public boolean isActivityWindow() {
+ return mActivityRecord != null;
+ }
+
void setSecureLocked(boolean isSecure) {
ProtoLog.i(WM_SHOW_TRANSACTIONS, "SURFACE isSecure=%b: %s", isSecure, getName());
if (secureWindowState()) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 7ef49c4..ba89fda 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -2477,20 +2477,12 @@
im->setInputDeviceEnabled(deviceId, false);
}
-static void nativeSetPointerIconType(JNIEnv* env, jobject nativeImplObj, jint iconId) {
- // TODO(b/311416205): Remove
-}
-
static void nativeReloadPointerIcons(JNIEnv* env, jobject nativeImplObj) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
im->reloadPointerIcons();
}
-static void nativeSetCustomPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject iconObj) {
- // TODO(b/311416205): Remove
-}
-
static bool nativeSetPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject iconObj,
jint displayId, jint deviceId, jint pointerId,
jobject inputTokenObj) {
@@ -2812,10 +2804,7 @@
{"isInputDeviceEnabled", "(I)Z", (void*)nativeIsInputDeviceEnabled},
{"enableInputDevice", "(I)V", (void*)nativeEnableInputDevice},
{"disableInputDevice", "(I)V", (void*)nativeDisableInputDevice},
- {"setPointerIconType", "(I)V", (void*)nativeSetPointerIconType},
{"reloadPointerIcons", "()V", (void*)nativeReloadPointerIcons},
- {"setCustomPointerIcon", "(Landroid/view/PointerIcon;)V",
- (void*)nativeSetCustomPointerIcon},
{"setPointerIcon", "(Landroid/view/PointerIcon;IIILandroid/os/IBinder;)Z",
(void*)nativeSetPointerIcon},
{"setPointerIconVisibility", "(IZ)V", (void*)nativeSetPointerIconVisibility},
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 375fc5a..9eb7b22 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -21605,12 +21605,9 @@
== HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
}
- if (Flags.headlessSingleMinTargetSdk()
- && mInjector.userManagerIsHeadlessSystemUserMode()
- && isSingleUserMode
- && !mInjector.isChangeEnabled(
- PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(),
- caller.getUserId())) {
+ if (Flags.headlessSingleUserFixes() && mInjector.userManagerIsHeadlessSystemUserMode()
+ && isSingleUserMode && !mInjector.isChangeEnabled(
+ PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(), caller.getUserId())) {
throw new IllegalStateException("Device admin is not targeting Android V.");
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
index 4bf3ff4..09eef45 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java
@@ -196,19 +196,27 @@
Binder.withCleanCallingIdentity(() -> {
PackageManagerInternal pmi =
LocalServices.getService(PackageManagerInternal.class);
+ AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
+
pmi.setOwnerProtectedPackages(userId,
packages == null ? null : packages.stream().toList());
LocalServices.getService(UsageStatsManagerInternal.class)
.setAdminProtectedPackages(
packages == null ? null : new ArraySet<>(packages), userId);
- if (Flags.disallowUserControlBgUsageFix()) {
- if (packages == null) {
- return;
+ if (packages == null || packages.isEmpty()) {
+ return;
+ }
+
+ for (int user : resolveUsers(userId)) {
+ if (Flags.disallowUserControlBgUsageFix()) {
+ setBgUsageAppOp(packages, pmi, user, appOpsManager);
}
- final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
- resolveUsers(userId).forEach(
- user -> setBgUsageAppOp(packages, pmi, user, appOpsManager));
+ if (Flags.disallowUserControlStoppedStateFix()) {
+ for (String packageName : packages) {
+ pmi.setPackageStoppedState(packageName, false, user);
+ }
+ }
}
});
return true;
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 8755a80..cfe4e17 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -47,6 +47,7 @@
import android.content.pm.PackageManagerInternal;
import android.content.res.Configuration;
import android.content.res.Resources.Theme;
+import android.crashrecovery.flags.Flags;
import android.credentials.CredentialManager;
import android.database.sqlite.SQLiteCompatibilityWalFlags;
import android.database.sqlite.SQLiteGlobal;
@@ -1195,11 +1196,13 @@
mSystemServiceManager.startService(RecoverySystemService.Lifecycle.class);
t.traceEnd();
- // Now that we have the bare essentials of the OS up and running, take
- // note that we just booted, which might send out a rescue party if
- // we're stuck in a runtime restart loop.
- RescueParty.registerHealthObserver(mSystemContext);
- PackageWatchdog.getInstance(mSystemContext).noteBoot();
+ if (!Flags.recoverabilityDetection()) {
+ // Now that we have the bare essentials of the OS up and running, take
+ // note that we just booted, which might send out a rescue party if
+ // we're stuck in a runtime restart loop.
+ RescueParty.registerHealthObserver(mSystemContext);
+ PackageWatchdog.getInstance(mSystemContext).noteBoot();
+ }
// Manages LEDs and display backlight so we need it to bring up the display.
t.traceBegin("StartLightsService");
@@ -1469,9 +1472,12 @@
boolean enableVrService = context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE);
- // For debugging RescueParty
- if (Build.IS_DEBUGGABLE && SystemProperties.getBoolean("debug.crash_system", false)) {
- throw new RuntimeException();
+ if (!Flags.recoverabilityDetection()) {
+ // For debugging RescueParty
+ if (Build.IS_DEBUGGABLE
+ && SystemProperties.getBoolean("debug.crash_system", false)) {
+ throw new RuntimeException();
+ }
}
try {
@@ -2910,6 +2916,14 @@
mPackageManagerService.systemReady();
t.traceEnd();
+ if (Flags.recoverabilityDetection()) {
+ // Now that we have the essential services needed for rescue party, initialize
+ // RescuParty. note that we just booted, which might send out a rescue party if
+ // we're stuck in a runtime restart loop.
+ RescueParty.registerHealthObserver(mSystemContext);
+ PackageWatchdog.getInstance(mSystemContext).noteBoot();
+ }
+
t.traceBegin("MakeDisplayManagerServiceReady");
try {
// TODO: use boot phase and communicate this flag some other way
@@ -3313,6 +3327,14 @@
* are updated outside of OTA; and to avoid breaking dependencies from system into apexes.
*/
private void startApexServices(@NonNull TimingsTraceAndSlog t) {
+ if (Flags.recoverabilityDetection()) {
+ // For debugging RescueParty
+ if (Build.IS_DEBUGGABLE
+ && SystemProperties.getBoolean("debug.crash_system", false)) {
+ throw new RuntimeException();
+ }
+ }
+
t.traceBegin("startApexServices");
// TODO(b/192880996): get the list from "android" package, once the manifest entries
// are migrated to system manifest.
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index b155829..6499556 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -2046,8 +2046,20 @@
writer.println("Unknown app ID $appId.")
}
}
+ } else if (args[0] == "--package" && args.size == 2) {
+ val packageName = args[1]
+ service.getState {
+ val packageState = state.externalState.packageStates[packageName]
+ if (packageState != null) {
+ writer.dumpAppIdState(packageState.appId, state, indexedSetOf(packageName))
+ } else {
+ writer.println("Unknown package $packageName.")
+ }
+ }
} else {
- writer.println("Usage: dumpsys permission [--app-id APP_ID]")
+ writer.println(
+ "Usage: dumpsys permissionmgr [--app-id <APP_ID>] [--package <PACKAGE_NAME>]"
+ )
}
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 80f38eb..e5685c7 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1790,6 +1790,7 @@
verify(mHolder.animator).animateTo(eq(brightness),
/* linearSecondTarget= */ anyFloat(), /* rate= */ anyFloat(),
/* ignoreAnimationLimits= */ anyBoolean());
+ verify(mHolder.brightnessSetting).setBrightness(brightness);
}
@Test
diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
index 4a21645..42814e7 100644
--- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java
@@ -239,6 +239,9 @@
@Test
public void testBootLoopDetectionWithExecutionForAllRescueLevels() {
+ // this is old test where the flag needs to be disabled
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
RescueParty.onSettingsProviderPublished(mMockContext);
verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
any(Executor.class),
@@ -449,6 +452,9 @@
@Test
public void testNonPersistentAppCrashDetectionWithScopedResets() {
+ // this is old test where the flag needs to be disabled
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
RescueParty.onSettingsProviderPublished(mMockContext);
verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
any(Executor.class),
@@ -506,6 +512,9 @@
@Test
public void testNonDeviceConfigSettingsOnlyResetOncePerLevel() {
+ // this is old test where the flag needs to be disabled
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
RescueParty.onSettingsProviderPublished(mMockContext);
verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver),
any(Executor.class),
@@ -879,6 +888,9 @@
@Test
public void testBootLoopLevels() {
+ // this is old test where the flag needs to be disabled
+ mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION);
+
RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext);
assertEquals(observer.onBootLoop(0), PackageHealthObserverImpact.USER_IMPACT_LEVEL_0);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java
index 599b9cd..8e1e339 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/SettingsToPropertiesMapperTest.java
@@ -273,10 +273,8 @@
keyValueMap.put("namespace_1*flag_1", "true");
// case 2: existing prop, stage a different value
keyValueMap.put("namespace_1*flag_2", "false");
- // case 3: new prop, stage the non default value
+ // case 3: new prop
keyValueMap.put("namespace_2*flag_1", "true");
- // case 4: new prop, stage the default value
- keyValueMap.put("namespace_2*flag_2", "false");
Properties props = new Properties(namespace, keyValueMap);
HashMap<String, HashMap<String, String>> toStageProps =
@@ -290,11 +288,9 @@
String namespace_1_flag_1 = namespace_1_to_stage.get("flag_1");
String namespace_1_flag_2 = namespace_1_to_stage.get("flag_2");
String namespace_2_flag_1 = namespace_2_to_stage.get("flag_1");
- String namespace_2_flag_2 = namespace_2_to_stage.get("flag_2");
Assert.assertTrue(namespace_1_flag_1 == null);
Assert.assertTrue(namespace_1_flag_2 != null);
Assert.assertTrue(namespace_2_flag_1 != null);
- Assert.assertTrue(namespace_2_flag_2 == null);
Assert.assertTrue(namespace_1_flag_2.equals("false"));
Assert.assertTrue(namespace_2_flag_1.equals("true"));
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
index 11f20e3..d15c24b 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java
@@ -31,6 +31,7 @@
import static com.android.server.job.JobSchedulerService.sUptimeMillisClock;
import static com.android.server.job.Flags.FLAG_BATCH_ACTIVE_BUCKET_JOBS;
import static com.android.server.job.Flags.FLAG_BATCH_CONNECTIVITY_JOBS_PER_NETWORK;
+import static com.android.server.job.Flags.FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -74,6 +75,9 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
import com.android.server.AppStateTracker;
@@ -85,6 +89,8 @@
import com.android.server.job.controllers.ConnectivityController;
import com.android.server.job.controllers.JobStatus;
import com.android.server.job.controllers.QuotaController;
+import com.android.server.job.restrictions.JobRestriction;
+import com.android.server.job.restrictions.ThermalStatusRestriction;
import com.android.server.pm.UserManagerInternal;
import com.android.server.usage.AppStandbyInternal;
@@ -121,6 +127,9 @@
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private ChargingPolicyChangeListener mChargingPolicyChangeListener;
private class TestJobSchedulerService extends JobSchedulerService {
@@ -2385,6 +2394,108 @@
assertEquals(JobScheduler.PENDING_JOB_REASON_USER, mService.getPendingJobReason(job2b));
}
+ /**
+ * Unit tests {@link JobSchedulerService#checkIfRestricted(JobStatus)} with single {@link
+ * JobRestriction} registered.
+ */
+ @Test
+ public void testCheckIfRestrictedSingleRestriction() {
+ int bias = JobInfo.BIAS_BOUND_FOREGROUND_SERVICE;
+ JobStatus fgsJob =
+ createJobStatus(
+ "testCheckIfRestrictedSingleRestriction", createJobInfo(1).setBias(bias));
+ ThermalStatusRestriction mockThermalStatusRestriction =
+ mock(ThermalStatusRestriction.class);
+ mService.mJobRestrictions.clear();
+ mService.mJobRestrictions.add(mockThermalStatusRestriction);
+ when(mockThermalStatusRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+
+ synchronized (mService.mLock) {
+ assertEquals(mService.checkIfRestricted(fgsJob), mockThermalStatusRestriction);
+ }
+
+ when(mockThermalStatusRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+ synchronized (mService.mLock) {
+ assertNull(mService.checkIfRestricted(fgsJob));
+ }
+ }
+
+ /**
+ * Unit tests {@link JobSchedulerService#checkIfRestricted(JobStatus)} with multiple {@link
+ * JobRestriction} registered.
+ */
+ @Test
+ public void testCheckIfRestrictedMultipleRestrictions() {
+ int bias = JobInfo.BIAS_BOUND_FOREGROUND_SERVICE;
+ JobStatus fgsJob =
+ createJobStatus(
+ "testGetMinJobExecutionGuaranteeMs", createJobInfo(1).setBias(bias));
+ JobRestriction mock1JobRestriction = mock(JobRestriction.class);
+ JobRestriction mock2JobRestriction = mock(JobRestriction.class);
+ mService.mJobRestrictions.clear();
+ mService.mJobRestrictions.add(mock1JobRestriction);
+ mService.mJobRestrictions.add(mock2JobRestriction);
+
+ // Jobs will be restricted if any one of the registered {@link JobRestriction}
+ // reports true.
+ when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+ when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+ synchronized (mService.mLock) {
+ assertEquals(mService.checkIfRestricted(fgsJob), mock1JobRestriction);
+ }
+
+ when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+ when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+ synchronized (mService.mLock) {
+ assertEquals(mService.checkIfRestricted(fgsJob), mock2JobRestriction);
+ }
+
+ when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+ when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(false);
+ synchronized (mService.mLock) {
+ assertNull(mService.checkIfRestricted(fgsJob));
+ }
+
+ when(mock1JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+ when(mock2JobRestriction.isJobRestricted(fgsJob, bias)).thenReturn(true);
+ synchronized (mService.mLock) {
+ assertNotEquals(mService.checkIfRestricted(fgsJob), mock1JobRestriction);
+ }
+ }
+
+ /**
+ * Jobs with foreground service and top app biases must not be restricted when the flag is
+ * disabled.
+ */
+ @Test
+ @RequiresFlagsDisabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS)
+ public void testCheckIfRestricted_highJobBias_flagThermalRestrictionsToFgsJobsDisabled() {
+ JobStatus fgsJob =
+ createJobStatus(
+ "testCheckIfRestrictedJobBiasFgs",
+ createJobInfo(1).setBias(JobInfo.BIAS_FOREGROUND_SERVICE));
+ JobStatus topAppJob =
+ createJobStatus(
+ "testCheckIfRestrictedJobBiasTopApp",
+ createJobInfo(2).setBias(JobInfo.BIAS_TOP_APP));
+
+ synchronized (mService.mLock) {
+ assertNull(mService.checkIfRestricted(fgsJob));
+ assertNull(mService.checkIfRestricted(topAppJob));
+ }
+ }
+
+ /** Jobs with top app biases must not be restricted. */
+ @Test
+ public void testCheckIfRestricted_highJobBias() {
+ JobStatus topAppJob = createJobStatus(
+ "testCheckIfRestrictedJobBiasTopApp",
+ createJobInfo(1).setBias(JobInfo.BIAS_TOP_APP));
+ synchronized (mService.mLock) {
+ assertNull(mService.checkIfRestricted(topAppJob));
+ }
+ }
+
private void setBatteryLevel(int level) {
doReturn(level).when(mBatteryManagerInternal).getBatteryLevel();
mService.mBatteryStateTracker
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
index 754f409..c2c67e6 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java
@@ -28,6 +28,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.job.Flags.FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -43,7 +44,12 @@
import android.content.ComponentName;
import android.content.Context;
import android.os.PowerManager;
+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.provider.DeviceConfig;
+import android.util.DebugUtils;
import androidx.test.runner.AndroidJUnit4;
@@ -53,6 +59,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -76,6 +83,157 @@
@Mock
private JobSchedulerService mJobSchedulerService;
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ class JobStatusContainer {
+ public final JobStatus jobMinPriority;
+ public final JobStatus jobLowPriority;
+ public final JobStatus jobLowPriorityRunning;
+ public final JobStatus jobLowPriorityRunningLong;
+ public final JobStatus jobDefaultPriority;
+ public final JobStatus jobHighPriority;
+ public final JobStatus jobHighPriorityRunning;
+ public final JobStatus jobHighPriorityRunningLong;
+ public final JobStatus ejDowngraded;
+ public final JobStatus ej;
+ public final JobStatus ejRetried;
+ public final JobStatus ejRunning;
+ public final JobStatus ejRunningLong;
+ public final JobStatus ui;
+ public final JobStatus uiRetried;
+ public final JobStatus uiRunning;
+ public final JobStatus uiRunningLong;
+ public final JobStatus importantWhileForeground;
+ public final JobStatus importantWhileForegroundRunning;
+ public final JobStatus importantWhileForegroundRunningLong;
+ public final int[] allJobBiases = {
+ JobInfo.BIAS_ADJ_ALWAYS_RUNNING,
+ JobInfo.BIAS_ADJ_OFTEN_RUNNING,
+ JobInfo.BIAS_DEFAULT,
+ JobInfo.BIAS_SYNC_EXPEDITED,
+ JobInfo.BIAS_SYNC_INITIALIZATION,
+ JobInfo.BIAS_BOUND_FOREGROUND_SERVICE,
+ JobInfo.BIAS_FOREGROUND_SERVICE,
+ JobInfo.BIAS_TOP_APP
+ };
+ public final int[] biasesBelowFgs = {
+ JobInfo.BIAS_ADJ_ALWAYS_RUNNING,
+ JobInfo.BIAS_ADJ_OFTEN_RUNNING,
+ JobInfo.BIAS_DEFAULT,
+ JobInfo.BIAS_SYNC_EXPEDITED,
+ JobInfo.BIAS_SYNC_INITIALIZATION,
+ JobInfo.BIAS_BOUND_FOREGROUND_SERVICE
+ };
+ public final int[] thermalStatuses = {
+ THERMAL_STATUS_NONE,
+ THERMAL_STATUS_LIGHT,
+ THERMAL_STATUS_MODERATE,
+ THERMAL_STATUS_SEVERE,
+ THERMAL_STATUS_CRITICAL,
+ THERMAL_STATUS_EMERGENCY,
+ THERMAL_STATUS_SHUTDOWN
+ };
+
+ JobStatusContainer(String jobName, JobSchedulerService mJobSchedulerService) {
+ jobMinPriority =
+ createJobStatus(
+ jobName, createJobBuilder(1).setPriority(JobInfo.PRIORITY_MIN).build());
+ jobLowPriority =
+ createJobStatus(
+ jobName, createJobBuilder(2).setPriority(JobInfo.PRIORITY_LOW).build());
+ jobLowPriorityRunning =
+ createJobStatus(
+ jobName, createJobBuilder(3).setPriority(JobInfo.PRIORITY_LOW).build());
+ jobLowPriorityRunningLong =
+ createJobStatus(
+ jobName, createJobBuilder(9).setPriority(JobInfo.PRIORITY_LOW).build());
+ jobDefaultPriority =
+ createJobStatus(
+ jobName,
+ createJobBuilder(4).setPriority(JobInfo.PRIORITY_DEFAULT).build());
+ jobHighPriority =
+ createJobStatus(
+ jobName,
+ createJobBuilder(5).setPriority(JobInfo.PRIORITY_HIGH).build());
+ jobHighPriorityRunning =
+ createJobStatus(
+ jobName,
+ createJobBuilder(6).setPriority(JobInfo.PRIORITY_HIGH).build());
+ jobHighPriorityRunningLong =
+ createJobStatus(
+ jobName,
+ createJobBuilder(10).setPriority(JobInfo.PRIORITY_HIGH).build());
+ ejDowngraded = createJobStatus(jobName, createJobBuilder(7).setExpedited(true).build());
+ ej = spy(createJobStatus(jobName, createJobBuilder(8).setExpedited(true).build()));
+ ejRetried =
+ spy(createJobStatus(jobName, createJobBuilder(11).setExpedited(true).build()));
+ ejRunning =
+ spy(createJobStatus(jobName, createJobBuilder(12).setExpedited(true).build()));
+ ejRunningLong =
+ spy(createJobStatus(jobName, createJobBuilder(13).setExpedited(true).build()));
+ ui = spy(createJobStatus(jobName, createJobBuilder(14).build()));
+ uiRetried = spy(createJobStatus(jobName, createJobBuilder(15).build()));
+ uiRunning = spy(createJobStatus(jobName, createJobBuilder(16).build()));
+ uiRunningLong = spy(createJobStatus(jobName, createJobBuilder(17).build()));
+ importantWhileForeground = spy(createJobStatus(jobName, createJobBuilder(18)
+ .setImportantWhileForeground(true)
+ .build()));
+ importantWhileForegroundRunning = spy(createJobStatus(jobName, createJobBuilder(20)
+ .setImportantWhileForeground(true)
+ .build()));
+ importantWhileForegroundRunningLong = spy(createJobStatus(jobName, createJobBuilder(19)
+ .setImportantWhileForeground(true)
+ .build()));
+
+ when(ej.shouldTreatAsExpeditedJob()).thenReturn(true);
+ when(ejRetried.shouldTreatAsExpeditedJob()).thenReturn(true);
+ when(ejRunning.shouldTreatAsExpeditedJob()).thenReturn(true);
+ when(ejRunningLong.shouldTreatAsExpeditedJob()).thenReturn(true);
+ when(ui.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+ when(uiRetried.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+ when(uiRunning.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+ when(uiRunningLong.shouldTreatAsUserInitiatedJob()).thenReturn(true);
+ when(ejRetried.getNumPreviousAttempts()).thenReturn(1);
+ when(uiRetried.getNumPreviousAttempts()).thenReturn(2);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning))
+ .thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning))
+ .thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunningLong))
+ .thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunningLong))
+ .thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunning)).thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunningLong)).thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunning)).thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunningLong)).thenReturn(true);
+ when(mJobSchedulerService.isJobInOvertimeLocked(jobLowPriorityRunningLong))
+ .thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(importantWhileForegroundRunning))
+ .thenReturn(true);
+ when(mJobSchedulerService.isJobInOvertimeLocked(jobHighPriorityRunningLong))
+ .thenReturn(true);
+ when(mJobSchedulerService.isJobInOvertimeLocked(ejRunningLong)).thenReturn(true);
+ when(mJobSchedulerService.isJobInOvertimeLocked(uiRunningLong)).thenReturn(true);
+ when(mJobSchedulerService.isCurrentlyRunningLocked(importantWhileForegroundRunningLong))
+ .thenReturn(true);
+ when(mJobSchedulerService.isJobInOvertimeLocked(importantWhileForegroundRunningLong))
+ .thenReturn(true);
+ }
+ }
+
+ private boolean isJobRestricted(JobStatus status, int bias) {
+ return mThermalStatusRestriction.isJobRestricted(status, bias);
+ }
+
+ private static String debugTag(int bias, @PowerManager.ThermalStatus int status) {
+ return "Bias = "
+ + JobInfo.getBiasString(bias)
+ + " Thermal Status = "
+ + DebugUtils.valueToString(PowerManager.class, "THERMAL_STATUS_", status);
+ }
+
@Before
public void setUp() {
mMockingSession = mockitoSession()
@@ -156,169 +314,302 @@
assertEquals(THERMAL_STATUS_EMERGENCY, mThermalStatusRestriction.getThermalStatus());
}
+ /**
+ * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Thermal is in default state
+ */
@Test
- public void testIsJobRestricted() {
+ public void testIsJobRestrictedDefaultStates() {
mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_NONE);
+ JobStatusContainer jc = new JobStatusContainer("testIsJobRestricted", mJobSchedulerService);
- final JobStatus jobMinPriority = createJobStatus("testIsJobRestricted",
- createJobBuilder(1).setPriority(JobInfo.PRIORITY_MIN).build());
- final JobStatus jobLowPriority = createJobStatus("testIsJobRestricted",
- createJobBuilder(2).setPriority(JobInfo.PRIORITY_LOW).build());
- final JobStatus jobLowPriorityRunning = createJobStatus("testIsJobRestricted",
- createJobBuilder(3).setPriority(JobInfo.PRIORITY_LOW).build());
- final JobStatus jobLowPriorityRunningLong = createJobStatus("testIsJobRestricted",
- createJobBuilder(9).setPriority(JobInfo.PRIORITY_LOW).build());
- final JobStatus jobDefaultPriority = createJobStatus("testIsJobRestricted",
- createJobBuilder(4).setPriority(JobInfo.PRIORITY_DEFAULT).build());
- final JobStatus jobHighPriority = createJobStatus("testIsJobRestricted",
- createJobBuilder(5).setPriority(JobInfo.PRIORITY_HIGH).build());
- final JobStatus jobHighPriorityRunning = createJobStatus("testIsJobRestricted",
- createJobBuilder(6).setPriority(JobInfo.PRIORITY_HIGH).build());
- final JobStatus jobHighPriorityRunningLong = createJobStatus("testIsJobRestricted",
- createJobBuilder(10).setPriority(JobInfo.PRIORITY_HIGH).build());
- final JobStatus ejDowngraded = createJobStatus("testIsJobRestricted",
- createJobBuilder(7).setExpedited(true).build());
- final JobStatus ej = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(8).setExpedited(true).build()));
- final JobStatus ejRetried = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(11).setExpedited(true).build()));
- final JobStatus ejRunning = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(12).setExpedited(true).build()));
- final JobStatus ejRunningLong = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(13).setExpedited(true).build()));
- final JobStatus ui = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(14).build()));
- final JobStatus uiRetried = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(15).build()));
- final JobStatus uiRunning = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(16).build()));
- final JobStatus uiRunningLong = spy(createJobStatus("testIsJobRestricted",
- createJobBuilder(17).build()));
- when(ej.shouldTreatAsExpeditedJob()).thenReturn(true);
- when(ejRetried.shouldTreatAsExpeditedJob()).thenReturn(true);
- when(ejRunning.shouldTreatAsExpeditedJob()).thenReturn(true);
- when(ejRunningLong.shouldTreatAsExpeditedJob()).thenReturn(true);
- when(ui.shouldTreatAsUserInitiatedJob()).thenReturn(true);
- when(uiRetried.shouldTreatAsUserInitiatedJob()).thenReturn(true);
- when(uiRunning.shouldTreatAsUserInitiatedJob()).thenReturn(true);
- when(uiRunningLong.shouldTreatAsUserInitiatedJob()).thenReturn(true);
- when(ejRetried.getNumPreviousAttempts()).thenReturn(1);
- when(uiRetried.getNumPreviousAttempts()).thenReturn(2);
- when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunning)).thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunning))
- .thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(jobLowPriorityRunningLong))
- .thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(jobHighPriorityRunningLong))
- .thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunning)).thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(ejRunningLong)).thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunning)).thenReturn(true);
- when(mJobSchedulerService.isCurrentlyRunningLocked(uiRunningLong)).thenReturn(true);
- when(mJobSchedulerService.isJobInOvertimeLocked(jobLowPriorityRunningLong))
- .thenReturn(true);
- when(mJobSchedulerService.isJobInOvertimeLocked(jobHighPriorityRunningLong))
- .thenReturn(true);
- when(mJobSchedulerService.isJobInOvertimeLocked(ejRunningLong)).thenReturn(true);
- when(mJobSchedulerService.isJobInOvertimeLocked(uiRunningLong)).thenReturn(true);
+ for (int jobBias : jc.allJobBiases) {
+ assertFalse(isJobRestricted(jc.jobMinPriority, jobBias));
+ assertFalse(isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertFalse(isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertFalse(isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertFalse(isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertFalse(isJobRestricted(jc.importantWhileForeground, jobBias));
+ assertFalse(isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+ assertFalse(isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+ assertFalse(isJobRestricted(jc.ej, jobBias));
+ assertFalse(isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(isJobRestricted(jc.ejRunningLong, jobBias));
+ assertFalse(isJobRestricted(jc.ui, jobBias));
+ assertFalse(isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(isJobRestricted(jc.uiRunningLong, jobBias));
+ }
+ }
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ej));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ui));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+ /**
+ * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Top App and all
+ * Thermal states.
+ */
+ @Test
+ public void testIsJobRestrictedBiasTopApp() {
+ JobStatusContainer jc =
+ new JobStatusContainer("testIsJobRestrictedBiasTopApp", mJobSchedulerService);
- mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_LIGHT);
+ int jobBias = JobInfo.BIAS_TOP_APP;
+ for (int thermalStatus : jc.thermalStatuses) {
+ String msg = "Thermal Status = " + DebugUtils.valueToString(
+ PowerManager.class, "THERMAL_STATUS_", thermalStatus);
+ mStatusChangedListener.onThermalStatusChanged(thermalStatus);
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ej));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ui));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+ // No restrictions on any jobs
+ assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForeground, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ }
+ }
- mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_MODERATE);
+ /**
+ * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Foreground
+ * Service and all Thermal states.
+ */
+ @Test
+ @RequiresFlagsDisabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS)
+ public void testIsJobRestrictedBiasFgs_flagThermalRestrictionsToFgsJobsDisabled() {
+ JobStatusContainer jc =
+ new JobStatusContainer("testIsJobRestrictedBiasFgs", mJobSchedulerService);
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
- assertFalse(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ej));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ejRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
- assertFalse(mThermalStatusRestriction.isJobRestricted(ui));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRetried));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunning));
- assertFalse(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+ int jobBias = JobInfo.BIAS_FOREGROUND_SERVICE;
+ for (int thermalStatus : jc.thermalStatuses) {
+ String msg = "Thermal Status = " + DebugUtils.valueToString(
+ PowerManager.class, "THERMAL_STATUS_", thermalStatus);
+ mStatusChangedListener.onThermalStatusChanged(thermalStatus);
+ // No restrictions on any jobs
+ assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ }
+ }
- mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_SEVERE);
+ /**
+ * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is Foreground
+ * Service and all Thermal states.
+ */
+ @Test
+ @RequiresFlagsEnabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS)
+ public void testIsJobRestrictedBiasFgs_flagThermalRestrictionsToFgsJobsEnabled() {
+ JobStatusContainer jc =
+ new JobStatusContainer("testIsJobRestrictedBiasFgs", mJobSchedulerService);
+ int jobBias = JobInfo.BIAS_FOREGROUND_SERVICE;
+ for (int thermalStatus : jc.thermalStatuses) {
+ String msg = debugTag(jobBias, thermalStatus);
+ mStatusChangedListener.onThermalStatusChanged(thermalStatus);
+ if (thermalStatus >= THERMAL_STATUS_SEVERE) {
+ // Full restrictions on all jobs
+ assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ej, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ui, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ } else if (thermalStatus >= THERMAL_STATUS_MODERATE) {
+ // No restrictions on user related jobs
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ // Some restrictions on expedited jobs
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ // Some restrictions on high priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ // Some restructions on important while foreground jobs
+ assertFalse(isJobRestricted(jc.importantWhileForeground, jobBias));
+ assertFalse(isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+ assertTrue(isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+ // Full restriction on default priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ // Full restriction on low priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ // Full restriction on min priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ } else {
+ // thermalStatus < THERMAL_STATUS_MODERATE
+ // No restrictions on any job type
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForeground, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ }
+ }
+ }
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ej));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ui));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRetried));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+ /**
+ * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is less than
+ * Foreground Service and all Thermal states.
+ */
+ @Test
+ public void testIsJobRestrictedBiasLessThanFgs() {
+ JobStatusContainer jc =
+ new JobStatusContainer("testIsJobRestrictedBiasLessThanFgs", mJobSchedulerService);
- mStatusChangedListener.onThermalStatusChanged(THERMAL_STATUS_CRITICAL);
-
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobMinPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobLowPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobDefaultPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriority));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(jobHighPriorityRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejDowngraded));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ej));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRetried));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ejRunningLong));
- assertTrue(mThermalStatusRestriction.isJobRestricted(ui));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRetried));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunning));
- assertTrue(mThermalStatusRestriction.isJobRestricted(uiRunningLong));
+ for (int jobBias : jc.biasesBelowFgs) {
+ for (int thermalStatus : jc.thermalStatuses) {
+ String msg = debugTag(jobBias, thermalStatus);
+ mStatusChangedListener.onThermalStatusChanged(thermalStatus);
+ if (thermalStatus >= THERMAL_STATUS_SEVERE) {
+ // Full restrictions on all jobs
+ assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ej, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ui, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ } else if (thermalStatus >= THERMAL_STATUS_MODERATE) {
+ // No restrictions on user related jobs
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ // Some restrictions on expedited jobs
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ // Some restrictions on high priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ // Full restriction on default priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ // Full restriction on low priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ // Full restriction on min priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ } else if (thermalStatus >= THERMAL_STATUS_LIGHT) {
+ // No restrictions on any user related jobs
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ // No restrictions on any expedited jobs
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ // No restrictions on any high priority jobs
+ assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ // No restrictions on default priority jobs
+ assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ // Some restrictions on low priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ // Full restriction on min priority jobs
+ assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ } else { // THERMAL_STATUS_NONE
+ // No restrictions on any jobs
+ assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ej, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias));
+ assertFalse(msg, isJobRestricted(jc.ui, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias));
+ assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias));
+ }
+ }
+ }
}
private JobInfo.Builder createJobBuilder(int jobId) {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java
new file mode 100644
index 0000000..0716a5c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricDanglingReceiverTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.biometrics;
+
+import static com.android.server.biometrics.sensors.BiometricNotificationUtils.NOTIFICATION_ID;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.hardware.biometrics.BiometricsProtoEnums;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.testing.TestableContext;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BiometricDanglingReceiverTest {
+ @Rule
+ public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ private BiometricDanglingReceiver mBiometricDanglingReceiver;
+
+ @Rule
+ public final TestableContext mContext = spy(new TestableContext(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(), null));
+
+ @Mock
+ NotificationManager mNotificationManager;
+
+ @Mock
+ Intent mIntent;
+
+ @Captor
+ private ArgumentCaptor<Intent> mArgumentCaptor;
+
+ @Before
+ public void setUp() {
+ mContext.addMockSystemService(NotificationManager.class, mNotificationManager);
+ }
+
+ @Test
+ public void testFingerprintRegisterReceiver() {
+ initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ verify(mContext).registerReceiver(eq(mBiometricDanglingReceiver), any());
+ }
+
+ @Test
+ public void testFaceRegisterReceiver() {
+ initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FACE);
+ verify(mContext).registerReceiver(eq(mBiometricDanglingReceiver), any());
+ }
+
+ @Test
+ public void testOnReceive_fingerprintReEnrollLaunch() {
+ initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ when(mIntent.getAction()).thenReturn(
+ BiometricDanglingReceiver.ACTION_FINGERPRINT_RE_ENROLL_LAUNCH);
+
+ mBiometricDanglingReceiver.onReceive(mContext, mIntent);
+
+ // Verify fingerprint enroll process is launched.
+ verify(mContext).startActivity(mArgumentCaptor.capture());
+ assertThat(mArgumentCaptor.getValue().getAction())
+ .isEqualTo(Settings.ACTION_FINGERPRINT_ENROLL);
+
+ // Verify notification is canceled
+ verify(mNotificationManager).cancelAsUser("FingerprintReEnroll", NOTIFICATION_ID,
+ UserHandle.CURRENT);
+
+ // Verify receiver is unregistered after receiving the broadcast
+ verify(mContext).unregisterReceiver(mBiometricDanglingReceiver);
+ }
+
+ @Test
+ public void testOnReceive_faceReEnrollLaunch() {
+ initBroadcastReceiver(BiometricsProtoEnums.MODALITY_FACE);
+ when(mIntent.getAction()).thenReturn(
+ BiometricDanglingReceiver.ACTION_FACE_RE_ENROLL_LAUNCH);
+
+ mBiometricDanglingReceiver.onReceive(mContext, mIntent);
+
+ // Verify face enroll process is launched.
+ verify(mContext).startActivity(mArgumentCaptor.capture());
+ assertThat(mArgumentCaptor.getValue().getAction())
+ .isEqualTo(BiometricDanglingReceiver.FACE_SETTINGS_ACTION);
+
+ // Verify notification is canceled
+ verify(mNotificationManager).cancelAsUser("FaceReEnroll", NOTIFICATION_ID,
+ UserHandle.CURRENT);
+
+ // Verify receiver is unregistered after receiving the broadcast.
+ verify(mContext).unregisterReceiver(mBiometricDanglingReceiver);
+ }
+
+ private void initBroadcastReceiver(int modality) {
+ mBiometricDanglingReceiver = new BiometricDanglingReceiver(mContext, modality);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
index fc573d2..3789531 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java
@@ -1228,6 +1228,11 @@
Slog.d(TAG, "TestInternalEnumerateClient#startHalOperation");
onEnumerationResult(TEST_FINGERPRINT, 0 /* remaining */);
}
+
+ @Override
+ protected int getModality() {
+ return BiometricsProtoEnums.MODALITY_FINGERPRINT;
+ }
}
private static class TestRemovalClient extends RemovalClient<Fingerprint, Object> {
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
index 9845b58..d8bdd50 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceInternalEnumerateClientTest.java
@@ -20,8 +20,10 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -79,15 +81,21 @@
private final int mBiometricId = 1;
private final Face mFace = new Face("face", mBiometricId, 1 /* deviceId */);
private FaceInternalEnumerateClient mClient;
+ private boolean mNotificationSent;
@Before
public void setUp() {
when(mAidlSession.getSession()).thenReturn(mSession);
-
final List<Face> enrolled = new ArrayList<>();
enrolled.add(mFace);
- mClient = new FaceInternalEnumerateClient(mContext, () -> mAidlSession, mToken, USER_ID,
- TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger, mBiometricContext);
+ mClient = spy(new FaceInternalEnumerateClient(mContext, () -> mAidlSession, mToken, USER_ID,
+ TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger, mBiometricContext));
+
+ mNotificationSent = false;
+ doAnswer(invocation -> {
+ mNotificationSent = true;
+ return null;
+ }).when(mClient).sendDanglingNotification(anyList());
}
@Test
@@ -101,6 +109,7 @@
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+ assertThat(mNotificationSent).isFalse();
verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
verify(mCallback).onClientFinished(mClient, true);
}
@@ -116,6 +125,7 @@
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+ assertThat(mNotificationSent).isFalse();
verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
verify(mCallback, never()).onClientFinished(mClient, true);
}
@@ -131,6 +141,7 @@
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+ assertThat(mNotificationSent).isTrue();
verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, mBiometricId);
verify(mCallback).onClientFinished(mClient, true);
}
@@ -147,6 +158,7 @@
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(1);
+ assertThat(mNotificationSent).isFalse();
verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
verify(mCallback, never()).onClientFinished(mClient, true);
}
@@ -164,6 +176,7 @@
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(1);
+ assertThat(mNotificationSent).isTrue();
verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, mBiometricId);
verify(mCallback).onClientFinished(mClient, true);
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
index b5df836..fab1200 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalEnumerateClientTest.java
@@ -20,8 +20,10 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -80,15 +82,23 @@
private FingerprintInternalEnumerateClient mClient;
+ private boolean mNotificationSent;
+
@Before
public void setUp() {
when(mAidlSession.getSession()).thenReturn(mSession);
List<Fingerprint> enrolled = new ArrayList<>();
enrolled.add(new Fingerprint("one", 1, 1));
- mClient = new FingerprintInternalEnumerateClient(mContext, () -> mAidlSession, mToken,
+ mClient = spy(new FingerprintInternalEnumerateClient(mContext, () -> mAidlSession, mToken,
USER_ID, TAG, enrolled, mBiometricUtils, SENSOR_ID, mBiometricLogger,
- mBiometricContext);
+ mBiometricContext));
+
+ mNotificationSent = false;
+ doAnswer(invocation -> {
+ mNotificationSent = true;
+ return null;
+ }).when(mClient).sendDanglingNotification(anyList());
}
@Test
@@ -104,6 +114,7 @@
assertThat(mClient.getUnknownHALTemplates().stream()
.flatMap(x -> Stream.of(x.getBiometricId()))
.collect(Collectors.toList())).containsExactly(2, 3);
+ assertThat(mNotificationSent).isTrue();
verify(mBiometricUtils).removeBiometricForUser(mContext, USER_ID, 1);
verify(mCallback).onClientFinished(mClient, true);
}
@@ -118,6 +129,7 @@
verify(mSession).enumerateEnrollments();
assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(0);
+ assertThat(mNotificationSent).isFalse();
verify(mBiometricUtils, never()).removeBiometricForUser(any(), anyInt(), anyInt());
verify(mCallback).onClientFinished(mClient, true);
}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index fd880dd..178e7ec 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -33,7 +33,6 @@
import android.os.Handler;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.DisplayInfo;
@@ -41,13 +40,11 @@
import androidx.test.InstrumentationRegistry;
-import com.android.input.flags.Flags;
import com.android.server.LocalServices;
import com.android.server.input.InputManagerInternal;
import org.junit.After;
import org.junit.Before;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -60,9 +57,6 @@
private static final String LANGUAGE_TAG = "en-US";
private static final String LAYOUT_TYPE = "qwerty";
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
@Mock
private InputManagerInternal mInputManagerInternalMock;
@Mock
@@ -77,8 +71,6 @@
@Before
public void setUp() throws Exception {
- mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER);
-
MockitoAnnotations.initMocks(this);
mInputManagerMockHelper = new InputManagerMockHelper(
TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock);
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 2b81d78..da8961d 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -339,8 +339,6 @@
LocalServices.removeServiceForTest(DisplayManagerInternal.class);
LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
- mSetFlagsRule.enableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER);
-
doNothing().when(mInputManagerInternalMock)
.setMousePointerAccelerationEnabled(anyBoolean(), anyInt());
doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt());
diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
index 40ecaf1..7dd1847 100644
--- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerBackupRestoreTest.java
@@ -42,6 +42,7 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Binder;
+import android.os.Bundle;
import android.os.HandlerThread;
import android.os.LocaleList;
import android.os.Process;
@@ -488,7 +489,7 @@
setUpPackageInstalled(pkgNameA);
- mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, new Bundle());
verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
LocaleList.forLanguageTags(langTagsA), false, FrameworkStatsLog
@@ -504,7 +505,7 @@
setUpPackageInstalled(pkgNameB);
- mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, new Bundle());
verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID,
LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog
@@ -518,6 +519,66 @@
}
@Test
+ public void testRestore_appInstalledAfterSUW_restoresFromStage_ArchiveEnabled()
+ throws Exception {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HashMap<String, LocalesInfo> pkgLocalesMap = new HashMap<>();
+ String pkgNameA = "com.android.myAppA";
+ String pkgNameB = "com.android.myAppB";
+ String langTagsA = "ru";
+ String langTagsB = "hi,fr";
+ LocalesInfo localesInfoA = new LocalesInfo(langTagsA, false);
+ LocalesInfo localesInfoB = new LocalesInfo(langTagsB, true);
+ pkgLocalesMap.put(pkgNameA, localesInfoA);
+ pkgLocalesMap.put(pkgNameB, localesInfoB);
+ writeTestPayload(out, pkgLocalesMap);
+ setUpPackageNotInstalled(pkgNameA);
+ setUpPackageNotInstalled(pkgNameB);
+ setUpLocalesForPackage(pkgNameA, LocaleList.getEmptyLocaleList());
+ setUpLocalesForPackage(pkgNameB, LocaleList.getEmptyLocaleList());
+ setUpPackageNamesForSp(new ArraySet<>());
+
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(Intent.EXTRA_ARCHIVAL, true);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, bundle);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, bundle);
+
+ mBackupHelper.stageAndApplyRestoredPayload(out.toByteArray(), DEFAULT_USER_ID);
+
+ verifyNothingRestored();
+
+ setUpPackageInstalled(pkgNameA);
+
+ mPackageMonitor.onPackageUpdateFinished(pkgNameA, DEFAULT_UID);
+
+ verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameA, DEFAULT_USER_ID,
+ LocaleList.forLanguageTags(langTagsA), false, FrameworkStatsLog
+ .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
+
+ mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameA, false, false);
+
+ verify(mMockSpEditor, times(0)).putStringSet(anyString(), any());
+
+ pkgLocalesMap.remove(pkgNameA);
+
+ verifyStageDataForUser(pkgLocalesMap, DEFAULT_CREATION_TIME_MILLIS, DEFAULT_USER_ID);
+
+ setUpPackageInstalled(pkgNameB);
+
+ mPackageMonitor.onPackageUpdateFinished(pkgNameB, DEFAULT_UID);
+
+ verify(mMockLocaleManagerService, times(1)).setApplicationLocales(pkgNameB, DEFAULT_USER_ID,
+ LocaleList.forLanguageTags(langTagsB), true, FrameworkStatsLog
+ .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_BACKUP_RESTORE);
+
+ mBackupHelper.persistLocalesModificationInfo(DEFAULT_USER_ID, pkgNameB, true, false);
+
+ verify(mMockSpEditor, times(1)).putStringSet(Integer.toString(DEFAULT_USER_ID),
+ new ArraySet<>(Arrays.asList(pkgNameB)));
+ checkStageDataDoesNotExist(DEFAULT_USER_ID);
+ }
+
+ @Test
public void testRestore_appInstalledAfterSUWAndLocalesAlreadySet_restoresNothing()
throws Exception {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -535,7 +596,7 @@
setUpPackageInstalled(DEFAULT_PACKAGE_NAME);
setUpLocalesForPackage(DEFAULT_PACKAGE_NAME, LocaleList.forLanguageTags("hi,mr"));
- mPackageMonitor.onPackageAdded(DEFAULT_PACKAGE_NAME, DEFAULT_UID);
+ mPackageMonitor.onPackageAddedWithExtras(DEFAULT_PACKAGE_NAME, DEFAULT_UID, new Bundle());
// Since locales are already set, we should not restore anything for it.
verifyNothingRestored();
@@ -612,7 +673,7 @@
DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.minusHours(1).toMillis());
setUpPackageInstalled(pkgNameA);
- mPackageMonitor.onPackageAdded(pkgNameA, DEFAULT_UID);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameA, DEFAULT_UID, new Bundle());
verify(mMockLocaleManagerService, times(1)).setApplicationLocales(
pkgNameA, DEFAULT_USER_ID, LocaleList.forLanguageTags(langTagsA), false,
@@ -627,7 +688,7 @@
DEFAULT_CREATION_TIME_MILLIS + RETENTION_PERIOD.plusSeconds(1).toMillis());
setUpPackageInstalled(pkgNameB);
- mPackageMonitor.onPackageAdded(pkgNameB, DEFAULT_UID);
+ mPackageMonitor.onPackageAddedWithExtras(pkgNameB, DEFAULT_UID, new Bundle());
verify(mMockLocaleManagerService, times(0)).setApplicationLocales(eq(pkgNameB), anyInt(),
any(), anyBoolean(), anyInt());
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
index 695faa5..39a2259 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackgroundActivityStartControllerTests.java
@@ -19,6 +19,8 @@
import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PENDING_INTENT;
import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_PERMISSION;
import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_VISIBLE_WINDOW;
+import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK;
+import static com.android.window.flags.Flags.balImprovedMetrics;
import static com.google.common.truth.Truth.assertThat;
@@ -145,6 +147,16 @@
}
@Override
+ boolean shouldLogStats(BalVerdict finalVerdict, BalState state) {
+ return true;
+ }
+
+ @Override
+ boolean shouldLogIntentActivity(BalVerdict finalVerdict, BalState state) {
+ return true;
+ }
+
+ @Override
BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
return mCallerVerdict.orElseGet(
() -> super.checkBackgroundActivityStartAllowedByCaller(state));
@@ -238,7 +250,12 @@
// assertions
assertThat(verdict.getCode()).isEqualTo(BackgroundActivityStartController.BAL_BLOCK);
- assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", BAL_BLOCK));
+ } else {
+ assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+ }
}
// Tests for BackgroundActivityStartController.checkBackgroundActivityStart
@@ -268,7 +285,12 @@
// assertions
assertThat(verdict).isEqualTo(BalVerdict.BLOCK);
- assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", BAL_BLOCK));
+ } else {
+ assertThat(mBalAllowedLogs).isEmpty(); // not allowed
+ }
}
@Test
@@ -298,7 +320,12 @@
// assertions
assertThat(verdict).isEqualTo(callerVerdict);
- assertThat(mBalAllowedLogs).isEmpty(); // non-critical exception
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", callerVerdict.getCode()));
+ } else {
+ assertThat(mBalAllowedLogs).isEmpty(); // non-critical exception
+ }
}
@Test
@@ -362,7 +389,13 @@
// assertions
assertThat(verdict).isEqualTo(callerVerdict);
- assertThat(mBalAllowedLogs).containsExactly(new BalAllowedLog("", callerVerdict.getCode()));
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", callerVerdict.getCode()));
+ } else {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("", callerVerdict.getCode()));
+ }
}
@Test
@@ -398,7 +431,12 @@
// assertions
assertThat(verdict).isEqualTo(BalVerdict.BLOCK);
- assertThat(mBalAllowedLogs).isEmpty();
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", BAL_BLOCK));
+ } else {
+ assertThat(mBalAllowedLogs).isEmpty();
+ }
}
@Test
@@ -430,7 +468,12 @@
// assertions
assertThat(verdict).isEqualTo(callerVerdict);
- assertThat(mBalAllowedLogs).isEmpty();
+ if (balImprovedMetrics()) {
+ assertThat(mBalAllowedLogs).containsExactly(
+ new BalAllowedLog("package.app3/someClass", callerVerdict.getCode()));
+ } else {
+ assertThat(mBalAllowedLogs).isEmpty();
+ }
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 44d1b54..87395a1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -113,15 +113,10 @@
import android.metrics.LogMaker;
import android.os.Binder;
import android.os.RemoteException;
-import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.ArraySet;
-import android.util.DisplayMetrics;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.DisplayInfo;
@@ -131,7 +126,6 @@
import android.view.ISystemGestureExclusionListener;
import android.view.IWindowManager;
import android.view.InsetsState;
-import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
@@ -151,7 +145,6 @@
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.wm.utils.WmDisplayCutout;
-import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -178,10 +171,6 @@
@RunWith(WindowTestRunner.class)
public class DisplayContentTests extends WindowTestsBase {
- @Rule
- public final CheckFlagsRule mCheckFlagsRule =
- DeviceFlagsValueProvider.createCheckFlagsRule();
-
@SetupWindows(addAllCommonWindows = true)
@Test
public void testForAllWindows() {
@@ -514,44 +503,6 @@
assertEquals(currentConfig.fontScale, globalConfig.fontScale, 0.1 /* delta */);
}
- /**
- * Tests tapping on a root task in different display results in window gaining focus.
- */
- @Test
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_REMOVE_POINTER_EVENT_TRACKING_IN_WM)
- public void testInputEventBringsCorrectDisplayInFocus() {
- DisplayContent dc0 = mWm.getDefaultDisplayContentLocked();
- // Create a second display
- final DisplayContent dc1 = createNewDisplay();
-
- // Add root task with activity.
- final Task rootTask0 = createTask(dc0);
- final Task task0 = createTaskInRootTask(rootTask0, 0 /* userId */);
- final ActivityRecord activity = createNonAttachedActivityRecord(dc0);
- task0.addChild(activity, 0);
- dc0.configureDisplayPolicy();
- assertNotNull(dc0.mTapDetector);
-
- final Task rootTask1 = createTask(dc1);
- final Task task1 = createTaskInRootTask(rootTask1, 0 /* userId */);
- final ActivityRecord activity1 = createNonAttachedActivityRecord(dc0);
- task1.addChild(activity1, 0);
- dc1.configureDisplayPolicy();
- assertNotNull(dc1.mTapDetector);
-
- // tap on primary display.
- tapOnDisplay(dc0);
- // Check focus is on primary display.
- assertEquals(mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus,
- dc0.findFocusedWindow());
-
- // Tap on secondary display.
- tapOnDisplay(dc1);
- // Check focus is on secondary.
- assertEquals(mWm.mRoot.getTopFocusedDisplayContent().mCurrentFocus,
- dc1.findFocusedWindow());
- }
-
@Test
public void testFocusedWindowMultipleDisplays() {
doTestFocusedWindowMultipleDisplays(false /* perDisplayFocusEnabled */, Q);
@@ -2959,33 +2910,4 @@
throw new RuntimeException(e);
}
}
-
- private void tapOnDisplay(final DisplayContent dc) {
- final DisplayMetrics dm = dc.getDisplayMetrics();
- final float x = dm.widthPixels / 2;
- final float y = dm.heightPixels / 2;
- final long downTime = SystemClock.uptimeMillis();
- final long eventTime = SystemClock.uptimeMillis() + 100;
- // sending ACTION_DOWN
- final MotionEvent downEvent = MotionEvent.obtain(
- downTime,
- downTime,
- MotionEvent.ACTION_DOWN,
- x,
- y,
- 0 /*metaState*/);
- downEvent.setDisplayId(dc.getDisplayId());
- dc.mTapDetector.onPointerEvent(downEvent);
-
- // sending ACTION_UP
- final MotionEvent upEvent = MotionEvent.obtain(
- downTime,
- eventTime,
- MotionEvent.ACTION_UP,
- x,
- y,
- 0 /*metaState*/);
- upEvent.setDisplayId(dc.getDisplayId());
- dc.mTapDetector.onPointerEvent(upEvent);
- }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
index 37de51e..4fc222b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java
@@ -95,10 +95,6 @@
}
@Override
- public void updatePointerIcon(float x, float y) throws RemoteException {
- }
-
- @Override
public void dispatchWindowShown() throws RemoteException {
}
@@ -128,4 +124,9 @@
public void hideInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken)
throws RemoteException {
}
+
+ @Override
+ public void dumpWindow(ParcelFileDescriptor pfd) {
+
+ }
}
diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java
index 9d14290..2e93cba 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsService.java
@@ -654,7 +654,10 @@
}
} else if (Intent.ACTION_USER_STARTED.equals(action)) {
if (userId >= 0) {
- mHandler.obtainMessage(MSG_USER_STARTED, userId, 0).sendToTarget();
+ if (!Flags.disableIdleCheck() || userId > 0) {
+ // Don't check idle state for USER_SYSTEM during the boot up.
+ mHandler.obtainMessage(MSG_USER_STARTED, userId, 0).sendToTarget();
+ }
}
}
}
@@ -2013,6 +2016,8 @@
+ ": " + Flags.useParceledList());
pw.println(" " + Flags.FLAG_FILTER_BASED_EVENT_QUERY_API
+ ": " + Flags.filterBasedEventQueryApi());
+ pw.println(" " + Flags.FLAG_DISABLE_IDLE_CHECK
+ + ": " + Flags.disableIdleCheck());
final int[] userIds;
synchronized (mLock) {
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 0fc9d6f..709f58d 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -28,14 +28,11 @@
import android.os.SystemClock
import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
-import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.provider.Settings
import android.view.View.OnKeyListener
-import android.view.Display
import android.view.InputDevice
import android.view.KeyEvent
-import android.view.PointerIcon
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.test.mock.MockContentResolver
@@ -44,7 +41,6 @@
import com.google.common.truth.Truth.assertThat
import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
import org.junit.After
-import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -53,22 +49,16 @@
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito.`when`
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.junit.MockitoJUnit
import org.mockito.stubbing.OngoingStubbing
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
/**
* Tests for {@link InputManagerService}.
@@ -179,203 +169,6 @@
localService.setDisplayViewports(viewports)
verify(native).setDisplayViewports(any(Array<DisplayViewport>::class.java))
verify(native).setPointerDisplayId(displayId)
-
- val x = 42f
- val y = 314f
- service.onPointerDisplayIdChanged(displayId, x, y)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(displayId, x, y)
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun testSetVirtualMousePointerDisplayId() {
- // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked
- // until the native callback happens.
- var countDownLatch = CountDownLatch(1)
- val overrideDisplayId = 123
- Thread {
- assertTrue("Setting virtual pointer display should succeed",
- localService.setVirtualMousePointerDisplayId(overrideDisplayId))
- countDownLatch.countDown()
- }.start()
- assertFalse("Setting virtual pointer display should block",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
-
- val x = 42f
- val y = 314f
- service.onPointerDisplayIdChanged(overrideDisplayId, x, y)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, x, y)
- assertTrue("Native callback unblocks calling thread",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
- verify(native).setPointerDisplayId(overrideDisplayId)
-
- // Ensure that setting the same override again succeeds immediately.
- assertTrue("Setting the same virtual mouse pointer displayId again should succeed",
- localService.setVirtualMousePointerDisplayId(overrideDisplayId))
-
- // Ensure that we did not query WM for the pointerDisplayId when setting the override
- verify(wmCallbacks, never()).pointerDisplayId
-
- // Unset the virtual mouse pointer displayId, and ensure that we query WM for the new
- // pointer displayId and the calling thread is blocked until the native callback happens.
- countDownLatch = CountDownLatch(1)
- val pointerDisplayId = 42
- `when`(wmCallbacks.pointerDisplayId).thenReturn(pointerDisplayId)
- Thread {
- assertTrue("Unsetting virtual mouse pointer displayId should succeed",
- localService.setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY))
- countDownLatch.countDown()
- }.start()
- assertFalse("Unsetting virtual mouse pointer displayId should block",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
-
- service.onPointerDisplayIdChanged(pointerDisplayId, x, y)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(pointerDisplayId, x, y)
- assertTrue("Native callback unblocks calling thread",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
- verify(native).setPointerDisplayId(pointerDisplayId)
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun testSetVirtualMousePointerDisplayId_unsuccessfulUpdate() {
- // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked
- // until the native callback happens.
- val countDownLatch = CountDownLatch(1)
- val overrideDisplayId = 123
- Thread {
- assertFalse("Setting virtual pointer display should be unsuccessful",
- localService.setVirtualMousePointerDisplayId(overrideDisplayId))
- countDownLatch.countDown()
- }.start()
- assertFalse("Setting virtual pointer display should block",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
-
- val x = 42f
- val y = 314f
- // Assume the native callback updates the pointerDisplayId to the incorrect value.
- service.onPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(Display.INVALID_DISPLAY, x, y)
- assertTrue("Native callback unblocks calling thread",
- countDownLatch.await(100, TimeUnit.MILLISECONDS))
- verify(native).setPointerDisplayId(overrideDisplayId)
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun testSetVirtualMousePointerDisplayId_competingRequests() {
- val firstRequestSyncLatch = CountDownLatch(1)
- doAnswer {
- firstRequestSyncLatch.countDown()
- }.`when`(native).setPointerDisplayId(anyInt())
-
- val firstRequestLatch = CountDownLatch(1)
- val firstOverride = 123
- Thread {
- assertFalse("Setting virtual pointer display from thread 1 should be unsuccessful",
- localService.setVirtualMousePointerDisplayId(firstOverride))
- firstRequestLatch.countDown()
- }.start()
- assertFalse("Setting virtual pointer display should block",
- firstRequestLatch.await(100, TimeUnit.MILLISECONDS))
-
- assertTrue("Wait for first thread's request should succeed",
- firstRequestSyncLatch.await(100, TimeUnit.MILLISECONDS))
-
- val secondRequestLatch = CountDownLatch(1)
- val secondOverride = 42
- Thread {
- assertTrue("Setting virtual mouse pointer from thread 2 should be successful",
- localService.setVirtualMousePointerDisplayId(secondOverride))
- secondRequestLatch.countDown()
- }.start()
- assertFalse("Setting virtual mouse pointer should block",
- secondRequestLatch.await(100, TimeUnit.MILLISECONDS))
-
- val x = 42f
- val y = 314f
- // Assume the native callback updates directly to the second request.
- service.onPointerDisplayIdChanged(secondOverride, x, y)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(secondOverride, x, y)
- assertTrue("Native callback unblocks first thread",
- firstRequestLatch.await(100, TimeUnit.MILLISECONDS))
- assertTrue("Native callback unblocks second thread",
- secondRequestLatch.await(100, TimeUnit.MILLISECONDS))
- verify(native, times(2)).setPointerDisplayId(anyInt())
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun onDisplayRemoved_resetAllAdditionalInputProperties() {
- setVirtualMousePointerDisplayIdAndVerify(10)
-
- localService.setPointerIconVisible(false, 10)
- verify(native).setPointerIconVisibility(10, false)
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
- localService.setMousePointerAccelerationEnabled(false, 10)
- verify(native).setMousePointerAccelerationEnabled(10, false)
-
- service.onDisplayRemoved(10)
- verify(native).setPointerIconVisibility(10, true)
- verify(native).displayRemoved(eq(10))
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
- verify(native).setMousePointerAccelerationEnabled(10, true)
- verifyNoMoreInteractions(native)
-
- // This call should not block because the virtual mouse pointer override was never removed.
- localService.setVirtualMousePointerDisplayId(10)
-
- verify(native).setPointerDisplayId(eq(10))
- verifyNoMoreInteractions(native)
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun updateAdditionalInputPropertiesForOverrideDisplay() {
- setVirtualMousePointerDisplayIdAndVerify(10)
-
- localService.setPointerIconVisible(false, 10)
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
- verify(native).setPointerIconVisibility(10, false)
- localService.setMousePointerAccelerationEnabled(false, 10)
- verify(native).setMousePointerAccelerationEnabled(10, false)
-
- localService.setPointerIconVisible(true, 10)
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
- verify(native).setPointerIconVisibility(10, true)
- localService.setMousePointerAccelerationEnabled(true, 10)
- verify(native).setMousePointerAccelerationEnabled(10, true)
-
- localService.setPointerIconVisible(false, 20)
- verify(native).setPointerIconVisibility(20, false)
- localService.setMousePointerAccelerationEnabled(false, 20)
- verify(native).setMousePointerAccelerationEnabled(20, false)
- verifyNoMoreInteractions(native)
-
- clearInvocations(native)
- setVirtualMousePointerDisplayIdAndVerify(20)
-
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
- }
-
- @RequiresFlagsDisabled(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER)
- @Test
- fun setAdditionalInputPropertiesBeforeOverride() {
- localService.setPointerIconVisible(false, 10)
- localService.setMousePointerAccelerationEnabled(false, 10)
-
- verify(native).setPointerIconVisibility(10, false)
- verify(native).setMousePointerAccelerationEnabled(10, false)
- verifyNoMoreInteractions(native)
-
- setVirtualMousePointerDisplayIdAndVerify(10)
-
- verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
}
@Test
@@ -412,20 +205,6 @@
verify(native, times(2)).changeKeyboardLayoutAssociation()
}
- private fun setVirtualMousePointerDisplayIdAndVerify(overrideDisplayId: Int) {
- val thread = Thread { localService.setVirtualMousePointerDisplayId(overrideDisplayId) }
- thread.start()
-
- // Allow some time for the set override call to park while waiting for the native callback.
- Thread.sleep(100 /*millis*/)
- verify(native).setPointerDisplayId(overrideDisplayId)
-
- service.onPointerDisplayIdChanged(overrideDisplayId, 0f, 0f)
- testLooper.dispatchNext()
- verify(wmCallbacks).notifyPointerDisplayIdChanged(overrideDisplayId, 0f, 0f)
- thread.join(100 /*millis*/)
- }
-
private fun createVirtualDisplays(count: Int): List<VirtualDisplay> {
val displayManager: DisplayManager = context.getSystemService(
DisplayManager::class.java
diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java
index e3f84c1..f126000 100644
--- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java
+++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java
@@ -24,6 +24,7 @@
import android.app.Activity;
import android.os.Bundle;
+import android.os.Process;
import android.util.Log;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
@@ -47,8 +48,9 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- Log.v(TAG, "Create MainActivity as user " + getUserId() + " on display "
- + getDisplayId());
+ Log.v(TAG, "Create MainActivity as user "
+ + Process.myUserHandle().getIdentifier() + " on display "
+ + getDisplay().getDisplayId());
setContentView(R.layout.main_activity);
mImm = getSystemService(InputMethodManager.class);
mEditor = requireViewById(R.id.edit_text);
diff --git a/tools/hoststubgen/TEST_MAPPING b/tools/hoststubgen/TEST_MAPPING
index f6885e1..856e6ee 100644
--- a/tools/hoststubgen/TEST_MAPPING
+++ b/tools/hoststubgen/TEST_MAPPING
@@ -1,63 +1,7 @@
-// Keep the following two TEST_MAPPINGs in sync:
-// frameworks/base/ravenwood/TEST_MAPPING
-// frameworks/base/tools/hoststubgen/TEST_MAPPING
{
- "presubmit": [
- { "name": "tiny-framework-dump-test" },
- { "name": "hoststubgentest" },
- { "name": "hoststubgen-invoke-test" },
+ "imports": [
{
- "name": "RavenwoodMockitoTest_device"
- },
- {
- "name": "RavenwoodBivalentTest_device"
- },
- // The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING
- {
- "name": "SystemUIGoogleTests",
- "options": [
- {
- "exclude-annotation": "org.junit.Ignore"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- }
- ]
- }
- ],
- "presubmit-large": [
- {
- "name": "SystemUITests",
- "options": [
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- },
- {
- "exclude-annotation": "org.junit.Ignore"
- }
- ]
- }
- ],
- "ravenwood-presubmit": [
- {
- "name": "RavenwoodMinimumTest",
- "host": true
- },
- {
- "name": "RavenwoodMockitoTest",
- "host": true
- },
- {
- "name": "CtsUtilTestCasesRavenwood",
- "host": true
- },
- {
- "name": "RavenwoodCoreTest",
- "host": true
- },
- {
- "name": "RavenwoodBivalentTest",
- "host": true
+ "path": "frameworks/base/ravenwood"
}
]
}