Merge "Fix back gesture routed to wrong display" into main
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 0f3b1c3..033da2d 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -4944,10 +4944,14 @@
@Override
public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (action == null) {
+ return;
+ }
final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
synchronized (mLock) {
String pkgList[] = null;
- switch (intent.getAction()) {
+ switch (action) {
case Intent.ACTION_QUERY_PACKAGE_RESTART:
pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES);
for (String packageName : pkgList) {
diff --git a/core/api/current.txt b/core/api/current.txt
index 2c6e1cb..8eb8811 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -7015,6 +7015,7 @@
method public boolean areNotificationsEnabled();
method public boolean areNotificationsPaused();
method public boolean canNotifyAsPackage(@NonNull String);
+ method @FlaggedApi("android.app.api_rich_ongoing") public boolean canPostPromotedNotifications();
method public boolean canUseFullScreenIntent();
method public void cancel(int);
method public void cancel(@Nullable String, int);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index cc0354c..a1561c2 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6822,6 +6822,27 @@
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.ModuleProperties> CREATOR;
}
+ @FlaggedApi("android.media.soundtrigger.manager_api") public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getAudioCapabilities();
+ method @NonNull public byte[] getData();
+ method @NonNull public java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra> getKeyphrases();
+ method public boolean isAllowMultipleTriggers();
+ method public boolean isCaptureRequested();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR;
+ }
+
+ public static final class SoundTrigger.RecognitionConfig.Builder {
+ ctor public SoundTrigger.RecognitionConfig.Builder();
+ method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig build();
+ method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setAllowMultipleTriggers(boolean);
+ method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setAudioCapabilities(int);
+ method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setCaptureRequested(boolean);
+ method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setData(@Nullable byte[]);
+ method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setKeyphrases(@NonNull java.util.Collection<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>);
+ }
+
public static class SoundTrigger.RecognitionEvent {
method @Nullable public android.media.AudioFormat getCaptureFormat();
method public int getCaptureSession();
@@ -7772,10 +7793,16 @@
method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void deleteModel(java.util.UUID);
method public int getDetectionServiceOperationsTimeout();
method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.media.soundtrigger.SoundTriggerManager.Model getModel(java.util.UUID);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getModelState(@NonNull java.util.UUID);
method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModuleProperties getModuleProperties();
method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getParameter(@NonNull java.util.UUID, int);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public boolean isRecognitionActive(@NonNull java.util.UUID);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel);
method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModelParamRange queryParameter(@Nullable java.util.UUID, int);
method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int setParameter(@Nullable java.util.UUID, int, int);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int startRecognition(@NonNull java.util.UUID, @Nullable android.os.Bundle, @NonNull android.content.ComponentName, @NonNull android.hardware.soundtrigger.SoundTrigger.RecognitionConfig);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int stopRecognition(@NonNull java.util.UUID);
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int unloadSoundModel(@NonNull java.util.UUID);
method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void updateModel(android.media.soundtrigger.SoundTriggerManager.Model);
}
@@ -13020,6 +13047,8 @@
method public void onSuggestedReplySent(@NonNull String, @NonNull CharSequence, int);
method public final void unsnoozeNotification(@NonNull String);
field public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS";
+ field @FlaggedApi("android.service.notification.notification_classification") public static final String ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS = "android.service.notification.action.NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS";
+ field @FlaggedApi("android.service.notification.notification_classification") public static final String EXTRA_NOTIFICATION_KEY = "android.service.notification.extra.NOTIFICATION_KEY";
field public static final String FEEDBACK_RATING = "feedback.rating";
field public static final String SERVICE_INTERFACE = "android.service.notification.NotificationAssistantService";
field public static final int SOURCE_FROM_APP = 0; // 0x0
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 72a68f8..0a10920 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -398,6 +398,7 @@
method public android.content.ComponentName getEffectsSuppressor();
method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean);
+ method @FlaggedApi("android.app.api_rich_ongoing") public void setCanPostPromotedNotifications(@NonNull String, int, boolean);
method @RequiresPermission(android.Manifest.permission.MANAGE_NOTIFICATION_LISTENERS) public void setNotificationListenerAccessGranted(@NonNull android.content.ComponentName, boolean, boolean);
method @RequiresPermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) public void setToastRateLimitingEnabled(boolean);
method @FlaggedApi("android.app.modes_api") public boolean updateAutomaticZenRule(@NonNull String, @NonNull android.app.AutomaticZenRule, boolean);
@@ -1613,15 +1614,15 @@
public final class CameraManager {
method @NonNull public android.hardware.camera2.CameraCharacteristics getCameraCharacteristics(@NonNull String, boolean) throws android.hardware.camera2.CameraAccessException;
method public String[] getCameraIdListNoLazy() throws android.hardware.camera2.CameraAccessException;
- method @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String);
+ method @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static int getRotationOverrideInternal(@Nullable android.content.Context, @Nullable android.content.pm.PackageManager, @Nullable String);
method @RequiresPermission(android.Manifest.permission.CAMERA) public void openCamera(@NonNull String, boolean, @Nullable android.os.Handler, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
method @RequiresPermission(allOf={android.Manifest.permission.SYSTEM_CAMERA, android.Manifest.permission.CAMERA}) public void openCamera(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraDevice.StateCallback) throws android.hardware.camera2.CameraAccessException;
method public static boolean shouldOverrideToPortrait(@Nullable android.content.pm.PackageManager, @Nullable String);
field public static final String LANDSCAPE_TO_PORTRAIT_PROP = "camera.enable_landscape_to_portrait";
field public static final long OVERRIDE_CAMERA_LANDSCAPE_TO_PORTRAIT = 250678880L; // 0xef10e60L
- field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0
- field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1
- field @FlaggedApi("com.android.window.flags.camera_compat_for_freeform") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2
+ field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_NONE = 0; // 0x0
+ field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT = 1; // 0x1
+ field @FlaggedApi("com.android.window.flags.enable_camera_compat_for_desktop_windowing") public static final int ROTATION_OVERRIDE_ROTATION_ONLY = 2; // 0x2
}
public abstract static class CameraManager.AvailabilityCallback {
@@ -1885,17 +1886,9 @@
ctor public SoundTrigger.ModuleProperties(int, @NonNull String, @NonNull String, @NonNull String, int, @NonNull String, int, int, int, int, boolean, int, boolean, int, boolean, int);
}
- public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable {
- ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int);
+ @FlaggedApi("android.media.soundtrigger.manager_api") public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable {
+ ctor @Deprecated public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int);
ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[]);
- method public int describeContents();
- method public void writeToParcel(@NonNull android.os.Parcel, int);
- field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR;
- field public final boolean allowMultipleTriggers;
- field public final int audioCapabilities;
- field public final boolean captureRequested;
- field @NonNull public final byte[] data;
- field @NonNull public final android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[] keyphrases;
}
public static class SoundTrigger.RecognitionEvent {
@@ -2216,7 +2209,7 @@
public class SoundTriggerInstrumentation.RecognitionSession {
method public void clearRecognitionCallback();
method public int getAudioSession();
- method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig getRecognitionConfig();
+ method @FlaggedApi("android.media.soundtrigger.manager_api") @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig getRecognitionConfig();
method public void setRecognitionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.soundtrigger.SoundTriggerInstrumentation.RecognitionCallback);
method public void triggerAbortRecognition();
method public void triggerRecognitionEvent(@NonNull byte[], @Nullable java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>);
@@ -2227,7 +2220,6 @@
method @NonNull public android.media.soundtrigger.SoundTriggerManager createManagerForModule(@NonNull android.hardware.soundtrigger.SoundTrigger.ModuleProperties);
method @NonNull public android.media.soundtrigger.SoundTriggerManager createManagerForTestModule();
method @NonNull public static java.util.List<android.hardware.soundtrigger.SoundTrigger.ModuleProperties> listModuleProperties();
- method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel);
}
public static class SoundTriggerManager.Model {
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index fad289c..edcdb6c 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -1047,7 +1047,7 @@
private static String startComponentToString(@StartComponent int startComponent) {
return switch (startComponent) {
case START_COMPONENT_ACTIVITY -> "ACTIVITY";
- case START_COMPONENT_BROADCAST -> "SERVICE";
+ case START_COMPONENT_BROADCAST -> "BROADCAST";
case START_COMPONENT_CONTENT_PROVIDER -> "CONTENT PROVIDER";
case START_COMPONENT_SERVICE -> "SERVICE";
case START_COMPONENT_OTHER -> "OTHER";
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index b9fe356..8a54b5d 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -258,6 +258,7 @@
@EnforcePermission(allOf={"INTERACT_ACROSS_USERS", "ACCESS_NOTIFICATIONS"})
void unregisterCallNotificationEventListener(String packageName, in UserHandle userHandle, in ICallNotificationEventCallback listener);
- void setCanBePromoted(String pkg, int uid, boolean promote);
- boolean canBePromoted(String pkg, int uid);
+ void setCanBePromoted(String pkg, int uid, boolean promote, boolean fromUser);
+ boolean appCanBePromoted(String pkg, int uid);
+ boolean canBePromoted(String pkg);
}
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 83f9ff7..c7b84ae 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -953,6 +953,36 @@
}
/**
+ * Returns whether the calling app's properly formatted notifications can appear in a promoted
+ * format, which may result in higher ranking, appearances on additional surfaces, and richer
+ * presentation.
+ */
+ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+ public boolean canPostPromotedNotifications() {
+ INotificationManager service = getService();
+ try {
+ return service.canBePromoted(mContext.getPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Setter for {@link #canPostPromotedNotifications()}. Only callable by the OS.
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+ public void setCanPostPromotedNotifications(@NonNull String pkg, int uid, boolean allowed) {
+ INotificationManager service = getService();
+ try {
+ service.setCanBePromoted(pkg, uid, allowed, true);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Creates a group container for {@link NotificationChannel} objects.
*
* This can be used to rename an existing group.
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 0e761fc..c17da24 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -54,198 +54,8 @@
* LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing,
* but doesn't hold a lock across data fetches on query misses.
*
- * The intended use case is caching frequently-read, seldom-changed information normally
- * retrieved across interprocess communication. Imagine that you've written a user birthday
- * information daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface
- * over binder. That binder interface looks something like this:
- *
- * <pre>
- * parcelable Birthday {
- * int month;
- * int day;
- * }
- * interface IUserBirthdayService {
- * Birthday getUserBirthday(int userId);
- * }
- * </pre>
- *
- * Suppose the service implementation itself looks like this...
- *
- * <pre>
- * public class UserBirthdayServiceImpl implements IUserBirthdayService {
- * private final HashMap<Integer, Birthday%> mUidToBirthday;
- * {@literal @}Override
- * public synchronized Birthday getUserBirthday(int userId) {
- * return mUidToBirthday.get(userId);
- * }
- * private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) {
- * mUidToBirthday.clear();
- * mUidToBirthday.putAll(uidToBirthday);
- * }
- * }
- * </pre>
- *
- * ... and we have a client in frameworks (loaded into every app process) that looks
- * like this:
- *
- * <pre>
- * public class ActivityThread {
- * ...
- * public Birthday getUserBirthday(int userId) {
- * return GetService("birthdayd").getUserBirthday(userId);
- * }
- * ...
- * }
- * </pre>
- *
- * With this code, every time an app calls {@code getUserBirthday(uid)}, we make a binder call
- * to the birthdayd process and consult its database of birthdays. If we query user birthdays
- * frequently, we do a lot of work that we don't have to do, since user birthdays
- * change infrequently.
- *
- * PropertyInvalidatedCache is part of a pattern for optimizing this kind of
- * information-querying code. Using {@code PropertyInvalidatedCache}, you'd write the client
- * this way:
- *
- * <pre>
- * public class ActivityThread {
- * ...
- * private final PropertyInvalidatedCache.QueryHandler<Integer, Birthday> mBirthdayQuery =
- * new PropertyInvalidatedCache.QueryHandler<Integer, Birthday>() {
- * {@literal @}Override
- * public Birthday apply(Integer) {
- * return GetService("birthdayd").getUserBirthday(userId);
- * }
- * };
- * private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache
- * private static final String BDAY_CACHE_KEY = "cache_key.birthdayd";
- * private final PropertyInvalidatedCache<Integer, Birthday%> mBirthdayCache = new
- * PropertyInvalidatedCache<Integer, Birthday%>(
- * BDAY_CACHE_MAX, MODULE_SYSTEM, "getUserBirthday", mBirthdayQuery);
- *
- * public void disableUserBirthdayCache() {
- * mBirthdayCache.disableForCurrentProcess();
- * }
- * public void invalidateUserBirthdayCache() {
- * mBirthdayCache.invalidateCache();
- * }
- * public Birthday getUserBirthday(int userId) {
- * return mBirthdayCache.query(userId);
- * }
- * ...
- * }
- * </pre>
- *
- * With this cache, clients perform a binder call to birthdayd if asking for a user's birthday
- * for the first time; on subsequent queries, we return the already-known Birthday object.
- *
- * The second parameter to the IpcDataCache constructor is a string that identifies the "module"
- * that owns the cache. There are some well-known modules (such as {@code MODULE_SYSTEM} but any
- * string is permitted. The third parameters is the name of the API being cached; this, too, can
- * any value. The fourth is the name of the cache. The cache is usually named after th API.
- * Some things you must know about the three strings:
- * <list>
- * <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}.
- * Usually, the SELinux rules permit a process to write a system property (and therefore
- * invalidate a cache) based on the wildcard {@code cache_key.<module>.*}. This means that
- * although the cache can be constructed with any module string, whatever string is chosen must be
- * consistent with the SELinux configuration.
- * <ul> The API name can be any string of alphanumeric characters. All caches with the same API
- * are invalidated at the same time. If a server supports several caches and all are invalidated
- * in common, then it is most efficient to assign the same API string to every cache.
- * <ul> The cache name can be any string. In debug output, the name is used to distiguish between
- * caches with the same API name. The cache name is also used when disabling caches in the
- * current process. So, invalidation is based on the module+api but disabling (which is generally
- * a once-per-process operation) is based on the cache name.
- * </list>
- *
- * User birthdays do occasionally change, so we have to modify the server to invalidate this
- * cache when necessary. That invalidation code looks like this:
- *
- * <pre>
- * public class UserBirthdayServiceImpl {
- * ...
- * public UserBirthdayServiceImpl() {
- * ...
- * ActivityThread.currentActivityThread().disableUserBirthdayCache();
- * ActivityThread.currentActivityThread().invalidateUserBirthdayCache();
- * }
- *
- * private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) {
- * mUidToBirthday.clear();
- * mUidToBirthday.putAll(uidToBirthday);
- * ActivityThread.currentActivityThread().invalidateUserBirthdayCache();
- * }
- * ...
- * }
- * </pre>
- *
- * The call to {@code PropertyInvalidatedCache.invalidateCache()} guarantees that all clients
- * will re-fetch birthdays from binder during consequent calls to
- * {@code ActivityThread.getUserBirthday()}. Because the invalidate call happens with the lock
- * held, we maintain consistency between different client views of the birthday state. The use
- * of PropertyInvalidatedCache in this idiomatic way introduces no new race conditions.
- *
- * PropertyInvalidatedCache has a few other features for doing things like incremental
- * enhancement of cached values and invalidation of multiple caches (that all share the same
- * property key) at once.
- *
- * {@code BDAY_CACHE_KEY} is the name of a property that we set to an opaque unique value each
- * time we update the cache. SELinux configuration must allow everyone to read this property
- * and it must allow any process that needs to invalidate the cache (here, birthdayd) to write
- * the property. (These properties conventionally begin with the "cache_key." prefix.)
- *
- * The {@code UserBirthdayServiceImpl} constructor calls {@code disableUserBirthdayCache()} so
- * that calls to {@code getUserBirthday} from inside birthdayd don't go through the cache. In
- * this local case, there's no IPC, so use of the cache is (depending on exact
- * circumstance) unnecessary.
- *
- * There may be queries for which it is more efficient to bypass the cache than to cache
- * the result. This would be true, for example, if some queries would require frequent
- * cache invalidation while other queries require infrequent invalidation. To expand on
- * the birthday example, suppose that there is a userId that signifies "the next
- * birthday". When passed this userId, the server returns the next birthday among all
- * users - this value changes as time advances. The userId value can be cached, but the
- * cache must be invalidated whenever a birthday occurs, and this invalidates all
- * birthdays. If there is a large number of users, invalidation will happen so often that
- * the cache provides no value.
- *
- * The class provides a bypass mechanism to handle this situation.
- * <pre>
- * public class ActivityThread {
- * ...
- * private final IpcDataCache.QueryHandler<Integer, Birthday> mBirthdayQuery =
- * new IpcDataCache.QueryHandler<Integer, Birthday>() {
- * {@literal @}Override
- * public Birthday apply(Integer) {
- * return GetService("birthdayd").getUserBirthday(userId);
- * }
- * {@literal @}Override
- * public boolean shouldBypassQuery(Integer userId) {
- * return userId == NEXT_BIRTHDAY;
- * }
- * };
- * ...
- * }
- * </pre>
- *
- * If the {@code shouldBypassQuery()} method returns true then the cache is not used for that
- * particular query. The {@code shouldBypassQuery()} method is not abstract and the default
- * implementation returns false.
- *
- * For security, there is a allowlist of processes that are allowed to invalidate a cache.
- * The allowlist includes normal runtime processes but does not include test processes.
- * Test processes must call {@code PropertyInvalidatedCache.disableForTestMode()} to disable
- * all cache activity in that process.
- *
- * Caching can be disabled completely by initializing {@code sEnabled} to false and rebuilding.
- *
- * To test a binder cache, create one or more tests that exercise the binder method. This
- * should be done twice: once with production code and once with a special image that sets
- * {@code DEBUG} and {@code VERIFY} true. In the latter case, verify that no cache
- * inconsistencies are reported. If a cache inconsistency is reported, however, it might be a
- * false positive. This happens if the server side data can be read and written non-atomically
- * with respect to cache invalidation.
+ * This interface is deprecated. New clients should use {@link IpcDataCache} instead. Internally,
+ * that class uses {@link PropertyInvalidatedCache} , but that design may change in the future.
*
* @param <Query> The class used to index cache entries: must be hashable and comparable
* @param <Result> The class holding cache entries; use a boxed primitive if possible
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index daa15f0..9be928f 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -213,19 +213,17 @@
* <a href="{@docRoot}guide/topics/admin/device-admin.html">Device Administration</a>
* developer guide.
*
- * <p id="devicepolicycontroller">Through <a href="#managed_provisioning">Managed Provisioning</a>,
- * Device Administrator apps can also be recognised as <b>
- Device Policy Controllers</b>. Device Policy Controllers can be one of
+ * <p id="devicepolicycontroller">Device Administrator apps can also be recognised as <b>
+ * Device Policy Controllers</b>. Device Policy Controllers can be one of
* two types:
* <ul>
* <li>A <i id="deviceowner">Device Owner</i>, which only ever exists on the
- * {@link UserManager#isSystemUser System User} or {@link UserManager#isMainUser Main User}, is
+ * {@link UserManager#isSystemUser System User} or Main User, is
* the most powerful type of Device Policy Controller and can affect policy across the device.
* <li>A <i id="profileowner">Profile Owner<i>, which can exist on any user, can
* affect policy on the user it is on, and when it is running on
* {@link UserManager#isProfile a profile} has
- * <a href="#profile-on-parent">limited</a> ability to affect policy on its
- * {@link UserManager#getProfileParent parent}.
+ * <a href="#profile-on-parent">limited</a> ability to affect policy on its parent.
* </ul>
*
* <p>Additional capabilities can be provided to Device Policy Controllers in
@@ -233,7 +231,7 @@
* <ul>
* <li>A Profile Owner on an <a href="#organization-owned">organization owned</a> device has access
* to additional abilities, both <a href="#profile-on-parent-organization-owned">affecting policy on the profile's</a>
- * {@link UserManager#getProfileParent parent} and also the profile itself.
+ * parent and also the profile itself.
* <li>A Profile Owner running on the {@link UserManager#isSystemUser System User} has access to
* additional capabilities which affect the {@link UserManager#isSystemUser System User} and
* also the whole device.
@@ -245,13 +243,12 @@
* Controller</a>.
*
* <p><a href="#permissions">Permissions</a> are generally only given to apps
- * fulfilling particular key roles on the device (such as managing {@link DeviceLockManager
-device locks}).
+ * fulfilling particular key roles on the device (such as managing
+ * {@link android.devicelock.DeviceLockManager device locks}).
*
* <p id="roleholder"><b>Device Policy Management Role Holder</b>
- * <p>One app on the device fulfills the {@link RoleManager#ROLE_DEVICE_POLICY_MANAGEMENT Device
-Policy Management Role} and is trusted with managing the overall state of
- * Device Policy. This has access to much more powerful methods than
+ * <p>One app on the device fulfills the Device Policy Management Role and is trusted with managing
+ * the overall state of Device Policy. This has access to much more powerful methods than
* <a href="#managingapps">managing apps</a>.
*
* <p id="querying"><b>Querying Device Policy</b>
@@ -273,7 +270,7 @@
*
* <p id="managed_profile">A <b>Managed Profile</b> enables data separation. For example to use
* a device both for personal and corporate usage. The managed profile and its
- * {@link UserManager#getProfileParent parent} share a launcher.
+ * parent share a launcher.
*
* <p id="affiliated"><b>Affiliation</b>
* <p>Using the {@link #setAffiliationIds} method, a
@@ -6643,7 +6640,7 @@
* @param flags May be 0 or {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY}.
* @throws SecurityException if the calling application does not own an active administrator
* that uses {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} and the does not hold
- * the {@link android.Manifest.permission#LOCK_DEVICE} permission, or
+ * the LOCK_DEVICE permission, or
* the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is passed by an
* application that is not a profile owner of a managed profile.
* @throws IllegalArgumentException if the {@link #FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY} flag is
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 108b5f4..b139017 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -31,6 +31,16 @@
}
flag {
+ name: "modes_ui_empty_shade"
+ namespace: "systemui"
+ description: "Shows mode that is currently blocking notifications in the empty shade; dependent on flags modes_api and modes_ui"
+ bug: "366003631"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "modes_ui_test"
namespace: "systemui"
description: "Guards new CTS tests for Modes; dependent on flags modes_api and modes_ui"
diff --git a/core/java/android/app/supervision/ISupervisionManager.aidl b/core/java/android/app/supervision/ISupervisionManager.aidl
index 8d25cad..4598421 100644
--- a/core/java/android/app/supervision/ISupervisionManager.aidl
+++ b/core/java/android/app/supervision/ISupervisionManager.aidl
@@ -21,5 +21,5 @@
* {@hide}
*/
interface ISupervisionManager {
- boolean isSupervisionEnabled();
+ boolean isSupervisionEnabledForUser(int userId);
}
diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java
index 8611a92..aee1cd9 100644
--- a/core/java/android/app/supervision/SupervisionManager.java
+++ b/core/java/android/app/supervision/SupervisionManager.java
@@ -17,6 +17,7 @@
package android.app.supervision;
import android.annotation.SystemService;
+import android.annotation.UserHandleAware;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.os.RemoteException;
@@ -45,13 +46,12 @@
*
* @hide
*/
+ @UserHandleAware
public boolean isSupervisionEnabled() {
try {
- return mService.isSupervisionEnabled();
+ return mService.isSupervisionEnabledForUser(mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
-
-
}
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index abb562d..d8142fd 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -1042,10 +1042,11 @@
}
/**
- * Get the available info about the AppWidget.
+ * Returns the {@link AppWidgetProviderInfo} for the specified AppWidget.
*
- * @return A appWidgetId. If the appWidgetId has not been bound to a provider yet, or
- * you don't have access to that appWidgetId, null is returned.
+ * @return Information regarding the provider of speficied widget, returns null if the
+ * appWidgetId has not been bound to a provider yet, or you don't have access
+ * to that widget.
*/
public AppWidgetProviderInfo getAppWidgetInfo(int appWidgetId) {
if (mService == null) {
@@ -1390,7 +1391,7 @@
*
* @param provider The {@link ComponentName} for the {@link
* android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget.
- * @param extras In not null, this is passed to the launcher app. For eg {@link
+ * @param extras IF not null, this is passed to the launcher app. e.g. {@link
* #EXTRA_APPWIDGET_PREVIEW} can be used for a custom preview.
* @param successCallback If not null, this intent will be sent when the widget is created.
*
diff --git a/core/java/android/content/pm/SharedLibraryInfo.java b/core/java/android/content/pm/SharedLibraryInfo.java
index 5acebf5..d77b2f5 100644
--- a/core/java/android/content/pm/SharedLibraryInfo.java
+++ b/core/java/android/content/pm/SharedLibraryInfo.java
@@ -94,7 +94,7 @@
private final String mPath;
private final String mPackageName;
private final String mName;
- private final List<String> mCodePaths;
+ private List<String> mCodePaths;
private final long mVersion;
private final @Type int mType;
@@ -282,6 +282,15 @@
}
/**
+ * Sets new all code paths for that library.
+ *
+ * @hide
+ */
+ public void setAllCodePaths(List<String> paths) {
+ mCodePaths = paths;
+ }
+
+ /**
* Add a library dependency to that library. Note that this
* should be called under the package manager lock.
*
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index 139ff65..160cbdf 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -293,4 +293,19 @@
description: "Feature flag to provide the new methods within launcher apps class to get packages."
bug: "363324203"
is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+ name: "remove_cross_user_permission_hack"
+ namespace: "package_manager_service"
+ description: "Feature flag to remove hack code of using PackageManager.MATCH_ANY_USER flag without cross user permission."
+ bug: "332664521"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "delete_packages_silently_backport"
+ namespace: "package_manager_service"
+ description: "Feature flag to enable the holder of SYSTEM_APP_PROTECTION_SERVICE role to silently delete packages. To be deprecated by delete_packages_silently."
+ bug: "361776825"
+}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 2162792..1b21bdf 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -181,7 +181,7 @@
* @hide
*/
@TestApi
- @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public static final int ROTATION_OVERRIDE_NONE = ICameraService.ROTATION_OVERRIDE_NONE;
/**
@@ -191,7 +191,7 @@
* @hide
*/
@TestApi
- @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public static final int ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT =
ICameraService.ROTATION_OVERRIDE_OVERRIDE_TO_PORTRAIT;
@@ -201,7 +201,7 @@
* @hide
*/
@TestApi
- @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public static final int ROTATION_OVERRIDE_ROTATION_ONLY =
ICameraService.ROTATION_OVERRIDE_ROTATION_ONLY;
@@ -1562,7 +1562,7 @@
*/
public static int getRotationOverride(@Nullable Context context,
@Nullable PackageManager packageManager, @Nullable String packageName) {
- if (com.android.window.flags.Flags.cameraCompatForFreeform()) {
+ if (com.android.window.flags.Flags.enableCameraCompatForDesktopWindowing()) {
return getRotationOverrideInternal(context, packageManager, packageName);
} else {
return shouldOverrideToPortrait(packageManager, packageName)
@@ -1574,7 +1574,7 @@
/**
* @hide
*/
- @FlaggedApi(com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@TestApi
public static int getRotationOverrideInternal(@Nullable Context context,
@Nullable PackageManager packageManager, @Nullable String packageName) {
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index a2d24f6..73b5d94 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -432,6 +432,11 @@
public abstract IntArray getDisplayGroupIds();
/**
+ * Get all available display ids.
+ */
+ public abstract IntArray getDisplayIds();
+
+ /**
* Called upon presentation started/ended on the display.
* @param displayId the id of the display where presentation started.
* @param isShown whether presentation is shown.
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index bdbec55..5ee61bc 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -48,49 +48,57 @@
public static final int KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL = 8;
public static final int KEY_GESTURE_TYPE_TOGGLE_TASKBAR = 9;
public static final int KEY_GESTURE_TYPE_TAKE_SCREENSHOT = 10;
- public static final int KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER = 11;
- public static final int KEY_GESTURE_TYPE_BRIGHTNESS_UP = 12;
- public static final int KEY_GESTURE_TYPE_BRIGHTNESS_DOWN = 13;
- public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP = 14;
- public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN = 15;
- public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE = 16;
- public static final int KEY_GESTURE_TYPE_VOLUME_UP = 17;
- public static final int KEY_GESTURE_TYPE_VOLUME_DOWN = 18;
- public static final int KEY_GESTURE_TYPE_VOLUME_MUTE = 19;
- public static final int KEY_GESTURE_TYPE_ALL_APPS = 20;
- public static final int KEY_GESTURE_TYPE_LAUNCH_SEARCH = 21;
- public static final int KEY_GESTURE_TYPE_LANGUAGE_SWITCH = 22;
- public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 23;
- public static final int KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK = 24;
- public static final int KEY_GESTURE_TYPE_SYSTEM_MUTE = 25;
- public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT = 26;
- public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT = 27;
- public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT = 28;
- public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT = 29;
- public static final int KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT = 30;
- public static final int KEY_GESTURE_TYPE_LOCK_SCREEN = 31;
- public static final int KEY_GESTURE_TYPE_OPEN_NOTES = 32;
- public static final int KEY_GESTURE_TYPE_TOGGLE_POWER = 33;
- public static final int KEY_GESTURE_TYPE_SYSTEM_NAVIGATION = 34;
- public static final int KEY_GESTURE_TYPE_SLEEP = 35;
- public static final int KEY_GESTURE_TYPE_WAKEUP = 36;
- public static final int KEY_GESTURE_TYPE_MEDIA_KEY = 37;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER = 38;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL = 39;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS = 40;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR = 41;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR = 42;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MUSIC = 43;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS = 44;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING = 45;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_GALLERY = 46;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FILES = 47;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_WEATHER = 48;
- public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS = 49;
- public static final int KEY_GESTURE_TYPE_LAUNCH_APPLICATION_BY_PACKAGE_NAME = 50;
- public static final int KEY_GESTURE_TYPE_DESKTOP_MODE = 51;
- public static final int KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION = 52;
- public static final int KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER = 53;
+ public static final int KEY_GESTURE_TYPE_SCREENSHOT_CHORD = 11;
+ public static final int KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER = 12;
+ public static final int KEY_GESTURE_TYPE_BRIGHTNESS_UP = 13;
+ public static final int KEY_GESTURE_TYPE_BRIGHTNESS_DOWN = 14;
+ public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP = 15;
+ public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN = 16;
+ public static final int KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE = 17;
+ public static final int KEY_GESTURE_TYPE_VOLUME_UP = 18;
+ public static final int KEY_GESTURE_TYPE_VOLUME_DOWN = 19;
+ public static final int KEY_GESTURE_TYPE_VOLUME_MUTE = 20;
+ public static final int KEY_GESTURE_TYPE_ALL_APPS = 21;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_SEARCH = 22;
+ public static final int KEY_GESTURE_TYPE_LANGUAGE_SWITCH = 23;
+ public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS = 24;
+ public static final int KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK = 25;
+ public static final int KEY_GESTURE_TYPE_SYSTEM_MUTE = 26;
+ public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT = 27;
+ public static final int KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT = 28;
+ public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT = 29;
+ public static final int KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT = 30;
+ public static final int KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT = 31;
+ public static final int KEY_GESTURE_TYPE_LOCK_SCREEN = 32;
+ public static final int KEY_GESTURE_TYPE_OPEN_NOTES = 33;
+ public static final int KEY_GESTURE_TYPE_TOGGLE_POWER = 34;
+ public static final int KEY_GESTURE_TYPE_SYSTEM_NAVIGATION = 35;
+ public static final int KEY_GESTURE_TYPE_SLEEP = 36;
+ public static final int KEY_GESTURE_TYPE_WAKEUP = 37;
+ public static final int KEY_GESTURE_TYPE_MEDIA_KEY = 38;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER = 39;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL = 40;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS = 41;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR = 42;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR = 43;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MUSIC = 44;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS = 45;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING = 46;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_GALLERY = 47;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FILES = 48;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_WEATHER = 49;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_DEFAULT_FITNESS = 50;
+ public static final int KEY_GESTURE_TYPE_LAUNCH_APPLICATION_BY_PACKAGE_NAME = 51;
+ public static final int KEY_GESTURE_TYPE_DESKTOP_MODE = 52;
+ public static final int KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION = 53;
+ public static final int KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER = 54;
+ public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD = 55;
+ public static final int KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD = 56;
+ public static final int KEY_GESTURE_TYPE_GLOBAL_ACTIONS = 57;
+ public static final int KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
+ public static final int KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT = 59;
+ public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT = 60;
+ public static final int KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS = 61;
public static final int FLAG_CANCELLED = 1;
@@ -116,6 +124,7 @@
KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
KEY_GESTURE_TYPE_TOGGLE_TASKBAR,
KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+ KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
KEY_GESTURE_TYPE_BRIGHTNESS_UP,
KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
@@ -158,7 +167,15 @@
KEY_GESTURE_TYPE_LAUNCH_APPLICATION_BY_PACKAGE_NAME,
KEY_GESTURE_TYPE_DESKTOP_MODE,
KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
- KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER
+ KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER,
+ KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+ KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD,
+ KEY_GESTURE_TYPE_GLOBAL_ACTIONS,
+ KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+ KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+ KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+
})
@Retention(RetentionPolicy.SOURCE)
public @interface KeyGestureType {
@@ -360,6 +377,7 @@
case KEY_GESTURE_TYPE_TOGGLE_TASKBAR:
return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TOGGLE_TASKBAR;
case KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
+ case KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__TAKE_SCREENSHOT;
case KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__OPEN_SHORTCUT_HELPER;
@@ -472,6 +490,8 @@
return "KEY_GESTURE_TYPE_TOGGLE_TASKBAR";
case KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
return "KEY_GESTURE_TYPE_TAKE_SCREENSHOT";
+ case KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
+ return "KEY_GESTURE_TYPE_SCREENSHOT_CHORD";
case KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
return "KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER";
case KEY_GESTURE_TYPE_BRIGHTNESS_UP:
@@ -558,6 +578,20 @@
return "KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION";
case KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER:
return "KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER";
+ case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+ return "KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD";
+ case KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
+ return "KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD";
+ case KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
+ return "KEY_GESTURE_TYPE_GLOBAL_ACTIONS";
+ case KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+ return "KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD";
+ case KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
+ return "KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT";
+ case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
+ return "KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT";
+ case KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
+ return "KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS";
default:
return Integer.toHexString(value);
}
diff --git a/core/java/android/hardware/soundtrigger/ConversionUtil.java b/core/java/android/hardware/soundtrigger/ConversionUtil.java
index 5c07fa4..22ae676 100644
--- a/core/java/android/hardware/soundtrigger/ConversionUtil.java
+++ b/core/java/android/hardware/soundtrigger/ConversionUtil.java
@@ -155,16 +155,16 @@
public static RecognitionConfig api2aidlRecognitionConfig(
SoundTrigger.RecognitionConfig apiConfig) {
RecognitionConfig aidlConfig = new RecognitionConfig();
- aidlConfig.captureRequested = apiConfig.captureRequested;
- // apiConfig.allowMultipleTriggers is ignored by the lower layers.
+ aidlConfig.captureRequested = apiConfig.isCaptureRequested();
+ // apiConfig.isAllowMultipleTriggers() is ignored by the lower layers.
aidlConfig.phraseRecognitionExtras =
- new PhraseRecognitionExtra[apiConfig.keyphrases.length];
- for (int i = 0; i < apiConfig.keyphrases.length; ++i) {
+ new PhraseRecognitionExtra[apiConfig.getKeyphrases().size()];
+ for (int i = 0; i < apiConfig.getKeyphrases().size(); ++i) {
aidlConfig.phraseRecognitionExtras[i] = api2aidlPhraseRecognitionExtra(
- apiConfig.keyphrases[i]);
+ apiConfig.getKeyphrases().get(i));
}
- aidlConfig.data = Arrays.copyOf(apiConfig.data, apiConfig.data.length);
- aidlConfig.audioCapabilities = api2aidlAudioCapabilities(apiConfig.audioCapabilities);
+ aidlConfig.data = Arrays.copyOf(apiConfig.getData(), apiConfig.getData().length);
+ aidlConfig.audioCapabilities = api2aidlAudioCapabilities(apiConfig.getAudioCapabilities());
return aidlConfig;
}
diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java
index 9f3e3ad..05e91e4 100644
--- a/core/java/android/hardware/soundtrigger/SoundTrigger.java
+++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java
@@ -63,6 +63,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
@@ -1513,50 +1514,60 @@
* A RecognitionConfig is provided to
* {@link SoundTriggerModule#startRecognition(int, RecognitionConfig)} to configure the
* recognition request.
- *
- * @hide
*/
- @TestApi
+ @FlaggedApi(android.media.soundtrigger.Flags.FLAG_MANAGER_API)
public static final class RecognitionConfig implements Parcelable {
- /** True if the DSP should capture the trigger sound and make it available for further
- * capture. */
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public final boolean captureRequested;
- /**
- * True if the service should restart listening after the DSP triggers.
- * Note: This config flag is currently used at the service layer rather than by the DSP.
- */
- public final boolean allowMultipleTriggers;
- /** List of all keyphrases in the sound model for which recognition should be performed with
- * options for each keyphrase. */
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- @NonNull
- @SuppressLint("ArrayReturn")
- public final KeyphraseRecognitionExtra keyphrases[];
- /** Opaque data for use by system applications who know about voice engine internals,
- * typically during enrollment. */
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- @NonNull
- public final byte[] data;
-
- /**
- * Bit field encoding of the AudioCapabilities
- * supported by the firmware.
- */
+ private final boolean mCaptureRequested;
+ private final boolean mAllowMultipleTriggers;
+ private final KeyphraseRecognitionExtra mKeyphrases[];
+ private final byte[] mData;
@ModuleProperties.AudioCapabilities
- public final int audioCapabilities;
+ private final int mAudioCapabilities;
+ /**
+ * Constructor for {@link RecognitionConfig} with {@code audioCapabilities} describes a
+ * config that can be used by
+ * {@link SoundTriggerModule#startRecognition(int, RecognitionConfig)}
+ *
+ * @deprecated should use builder-based constructor instead.
+ * TODO(b/368042125): remove this method.
+ * @param captureRequested Whether the DSP should capture the trigger sound.
+ * @param allowMultipleTriggers Whether the service should restart listening after the DSP
+ * triggers.
+ * @param keyphrases List of keyphrases in the sound model.
+ * @param data Opaque data for use by system applications who know about voice engine
+ * internals, typically during enrollment.
+ * @param audioCapabilities Bit field encoding of the AudioCapabilities.
+ *
+ * @hide
+ */
+ @Deprecated
+ @SuppressWarnings("Todo")
+ @TestApi
public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers,
@SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases,
@Nullable byte[] data, int audioCapabilities) {
- this.captureRequested = captureRequested;
- this.allowMultipleTriggers = allowMultipleTriggers;
- this.keyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0];
- this.data = data != null ? data : new byte[0];
- this.audioCapabilities = audioCapabilities;
+ this.mCaptureRequested = captureRequested;
+ this.mAllowMultipleTriggers = allowMultipleTriggers;
+ this.mKeyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0];
+ this.mData = data != null ? data : new byte[0];
+ this.mAudioCapabilities = audioCapabilities;
}
+ /**
+ * Constructor for {@link RecognitionConfig} without audioCapabilities. The
+ * audioCapabilities is set to 0.
+ *
+ * @param captureRequested Whether the DSP should capture the trigger sound.
+ * @param allowMultipleTriggers Whether the service should restart listening after the DSP
+ * triggers.
+ * @param keyphrases List of keyphrases in the sound model.
+ * @param data Opaque data for use by system applications.
+ *
+ * @hide
+ */
@UnsupportedAppUsage
+ @TestApi
public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers,
@SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases,
@Nullable byte[] data) {
@@ -1574,9 +1585,52 @@
}
};
+ /**
+ * Returns whether the DSP should capture the trigger sound and make it available for
+ * further capture.
+ */
+ public boolean isCaptureRequested() {
+ return mCaptureRequested;
+ }
+
+ /**
+ * Returns whether the service should restart listening after the DSP triggers.
+ *
+ * <p><b>Note:</b> This config flag is currently used at the service layer rather than by
+ * the DSP.
+ */
+ public boolean isAllowMultipleTriggers() {
+ return mAllowMultipleTriggers;
+ }
+
+ /**
+ * Gets all keyphrases in the sound model for which recognition should be performed with
+ * options for each keyphrase.
+ */
+ @NonNull
+ public List<KeyphraseRecognitionExtra> getKeyphrases() {
+ return Arrays.asList(mKeyphrases);
+ }
+
+ /**
+ * Opaque data.
+ *
+ * <p>For use by system applications who knows about voice engine internals, typically
+ * during enrollment.
+ */
+ @NonNull
+ public byte[] getData() {
+ return mData;
+ }
+
+ /** Bit field encoding of the AudioCapabilities supported by the firmware. */
+ public int getAudioCapabilities() {
+ return mAudioCapabilities;
+ }
+
private static RecognitionConfig fromParcel(Parcel in) {
- boolean captureRequested = in.readByte() == 1;
- boolean allowMultipleTriggers = in.readByte() == 1;
+ boolean captureRequested = in.readBoolean();
+ boolean allowMultipleTriggers = in.readBoolean();
KeyphraseRecognitionExtra[] keyphrases =
in.createTypedArray(KeyphraseRecognitionExtra.CREATOR);
byte[] data = in.readBlob();
@@ -1587,11 +1641,11 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeByte((byte) (captureRequested ? 1 : 0));
- dest.writeByte((byte) (allowMultipleTriggers ? 1 : 0));
- dest.writeTypedArray(keyphrases, flags);
- dest.writeBlob(data);
- dest.writeInt(audioCapabilities);
+ dest.writeBoolean(mCaptureRequested);
+ dest.writeBoolean(mAllowMultipleTriggers);
+ dest.writeTypedArray(mKeyphrases, flags);
+ dest.writeBlob(mData);
+ dest.writeInt(mAudioCapabilities);
}
@Override
@@ -1601,10 +1655,10 @@
@Override
public String toString() {
- return "RecognitionConfig [captureRequested=" + captureRequested
- + ", allowMultipleTriggers=" + allowMultipleTriggers + ", keyphrases="
- + Arrays.toString(keyphrases) + ", data=" + Arrays.toString(data)
- + ", audioCapabilities=" + Integer.toHexString(audioCapabilities) + "]";
+ return "RecognitionConfig [captureRequested=" + mCaptureRequested
+ + ", allowMultipleTriggers=" + mAllowMultipleTriggers + ", keyphrases="
+ + Arrays.toString(mKeyphrases) + ", data=" + Arrays.toString(mData)
+ + ", audioCapabilities=" + Integer.toHexString(mAudioCapabilities) + "]";
}
@Override
@@ -1616,19 +1670,19 @@
if (!(obj instanceof RecognitionConfig))
return false;
RecognitionConfig other = (RecognitionConfig) obj;
- if (captureRequested != other.captureRequested) {
+ if (mCaptureRequested != other.mCaptureRequested) {
return false;
}
- if (allowMultipleTriggers != other.allowMultipleTriggers) {
+ if (mAllowMultipleTriggers != other.mAllowMultipleTriggers) {
return false;
}
- if (!Arrays.equals(keyphrases, other.keyphrases)) {
+ if (!Arrays.equals(mKeyphrases, other.mKeyphrases)) {
return false;
}
- if (!Arrays.equals(data, other.data)) {
+ if (!Arrays.equals(mData, other.mData)) {
return false;
}
- if (audioCapabilities != other.audioCapabilities) {
+ if (mAudioCapabilities != other.mAudioCapabilities) {
return false;
}
return true;
@@ -1638,13 +1692,96 @@
public final int hashCode() {
final int prime = 31;
int result = 1;
- result = prime * result + (captureRequested ? 1 : 0);
- result = prime * result + (allowMultipleTriggers ? 1 : 0);
- result = prime * result + Arrays.hashCode(keyphrases);
- result = prime * result + Arrays.hashCode(data);
- result = prime * result + audioCapabilities;
+ result = prime * result + (mCaptureRequested ? 1 : 0);
+ result = prime * result + (mAllowMultipleTriggers ? 1 : 0);
+ result = prime * result + Arrays.hashCode(mKeyphrases);
+ result = prime * result + Arrays.hashCode(mData);
+ result = prime * result + mAudioCapabilities;
return result;
}
+
+ /**
+ * Builder class for {@link RecognitionConfig} objects.
+ */
+ public static final class Builder {
+ private boolean mCaptureRequested;
+ private boolean mAllowMultipleTriggers;
+ @Nullable private KeyphraseRecognitionExtra[] mKeyphrases;
+ @Nullable private byte[] mData;
+ private int mAudioCapabilities;
+
+ /**
+ * Constructs a new Builder with the default values.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Sets capture requested state.
+ * @param captureRequested The new requested state.
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setCaptureRequested(boolean captureRequested) {
+ mCaptureRequested = captureRequested;
+ return this;
+ }
+
+ /**
+ * Sets allow multiple triggers state.
+ * @param allowMultipleTriggers The new allow multiple triggers state.
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setAllowMultipleTriggers(boolean allowMultipleTriggers) {
+ mAllowMultipleTriggers = allowMultipleTriggers;
+ return this;
+ }
+
+ /**
+ * Sets the keyphrases field.
+ * @param keyphrases The new keyphrases.
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setKeyphrases(
+ @NonNull Collection<KeyphraseRecognitionExtra> keyphrases) {
+ mKeyphrases = keyphrases.toArray(new KeyphraseRecognitionExtra[keyphrases.size()]);
+ return this;
+ }
+
+ /**
+ * Sets the data field.
+ * @param data The new data.
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setData(@Nullable byte[] data) {
+ mData = data;
+ return this;
+ }
+
+ /**
+ * Sets the audio capabilities field.
+ * @param audioCapabilities The new audio capabilities.
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setAudioCapabilities(int audioCapabilities) {
+ mAudioCapabilities = audioCapabilities;
+ return this;
+ }
+
+ /**
+ * Combines all of the parameters that have been set and return a new
+ * {@link RecognitionConfig} object.
+ * @return a new {@link RecognitionConfig} object
+ */
+ public @NonNull RecognitionConfig build() {
+ RecognitionConfig config = new RecognitionConfig(
+ /* captureRequested= */ mCaptureRequested,
+ /* allowMultipleTriggers= */ mAllowMultipleTriggers,
+ /* keyphrases= */ mKeyphrases,
+ /* data= */ mData,
+ /* audioCapabilities= */ mAudioCapabilities);
+ return config;
+ }
+ };
}
/**
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 91cdf8d..1c9be6f 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -76,11 +76,15 @@
* PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION} before querying the service. If the feature is
* absent, {@link Context#getSystemService} may return null.
*/
-@SystemService(Context.VCN_MANAGEMENT_SERVICE)
+@SystemService(VcnManager.VCN_MANAGEMENT_SERVICE_STRING)
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)
public class VcnManager {
@NonNull private static final String TAG = VcnManager.class.getSimpleName();
+ // TODO: b/366598445: Expose and use Context.VCN_MANAGEMENT_SERVICE
+ /** @hide */
+ public static final String VCN_MANAGEMENT_SERVICE_STRING = "vcn_management";
+
/**
* Key for WiFi entry RSSI thresholds
*
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 1fef602..a698b9d 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -788,6 +788,12 @@
/** Parses an XML representation of BatteryUsageStats */
public static BatteryUsageStats createFromXml(TypedXmlPullParser parser)
throws XmlPullParserException, IOException {
+ return createBuilderFromXml(parser).build();
+ }
+
+ /** Parses an XML representation of BatteryUsageStats */
+ public static BatteryUsageStats.Builder createBuilderFromXml(TypedXmlPullParser parser)
+ throws XmlPullParserException, IOException {
Builder builder = null;
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
@@ -862,7 +868,7 @@
eventType = parser.next();
}
- return builder.build();
+ return builder;
}
@Override
@@ -978,10 +984,21 @@
*/
@NonNull
public BatteryUsageStats build() {
+ if (mBatteryConsumersCursorWindow == null) {
+ throw new IllegalStateException("Builder has been discarded");
+ }
return new BatteryUsageStats(this);
}
/**
+ * Close this builder without actually calling ".build()". Do not attempt
+ * to continue using the builder after this call.
+ */
+ public void discard() {
+ mBatteryConsumersCursorWindow.close();
+ }
+
+ /**
* Sets the battery capacity in milli-amp-hours.
*/
public Builder setBatteryCapacity(double batteryCapacityMah) {
diff --git a/core/java/android/os/BatteryUsageStatsQuery.java b/core/java/android/os/BatteryUsageStatsQuery.java
index a12606b..b533225 100644
--- a/core/java/android/os/BatteryUsageStatsQuery.java
+++ b/core/java/android/os/BatteryUsageStatsQuery.java
@@ -77,6 +77,8 @@
public static final int FLAG_BATTERY_USAGE_STATS_INCLUDE_POWER_STATE = 0x0040;
+ public static final int FLAG_BATTERY_USAGE_STATS_ACCUMULATED = 0x0080;
+
private static final long DEFAULT_MAX_STATS_AGE_MS = 5 * 60 * 1000;
private final int mFlags;
@@ -328,6 +330,15 @@
}
/**
+ * Requests the full continuously accumulated battery usage stats: across reboots
+ * and most battery stats resets.
+ */
+ public Builder accumulated() {
+ mFlags |= FLAG_BATTERY_USAGE_STATS_ACCUMULATED;
+ return this;
+ }
+
+ /**
* Requests to aggregate stored snapshots between the two supplied timestamps
* @param fromTimestamp Exclusive starting timestamp, as per System.currentTimeMillis()
* @param toTimestamp Inclusive ending timestamp, as per System.currentTimeMillis()
diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java
index 97e9f34..ed75491 100644
--- a/core/java/android/os/Binder.java
+++ b/core/java/android/os/Binder.java
@@ -1111,6 +1111,21 @@
}
/**
+ * Called whenever the stub implementation throws an exception which isn't propagated to the
+ * remote caller by the binder. If this method isn't overridden, this exception is swallowed,
+ * and some default return values are propagated to the caller.
+ *
+ * <br> <b> This should not throw. </b> Doing so would defeat the purpose of this handler, and
+ * suppress the exception it is handling.
+ *
+ * @param code The transaction code being handled
+ * @param e The exception which was thrown.
+ * @hide
+ */
+ protected void onUnhandledException(int code, int flags, Exception e) {
+ }
+
+ /**
* @param in The raw file descriptor that an input data stream can be read from.
* @param out The raw file descriptor that normal command messages should be written to.
* @param err The raw file descriptor that command error messages should be written to.
@@ -1408,10 +1423,15 @@
} else {
Log.w(TAG, "Caught a RuntimeException from the binder stub implementation.", e);
}
+ onUnhandledException(code, flags, e);
} else {
// Clear the parcel before writing the exception.
reply.setDataSize(0);
reply.setDataPosition(0);
+ // The writeException below won't do anything useful if this is the case.
+ if (Parcel.getExceptionCode(e) == 0) {
+ onUnhandledException(code, flags, e);
+ }
reply.writeException(e);
}
res = true;
diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java
index 0776cf4..e2a72dd 100644
--- a/core/java/android/os/IpcDataCache.java
+++ b/core/java/android/os/IpcDataCache.java
@@ -48,6 +48,20 @@
* LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing,
* but doesn't hold a lock across data fetches on query misses.
*
+ * Clients should be aware of the following commonly-seen issues:
+ * <ul>
+ *
+ * <li>Client calls will not go through the cache before the first invalidation signal is
+ * received. Therefore, servers should signal an invalidation as soon as they have data to offer to
+ * clients.
+ *
+ * <li>Cache invalidation is restricted to well-known processes, which means that test code cannot
+ * invalidate a cache. {@link #disableForTestMode()} and {@link #testPropertyName} must be used in
+ * test processes that attempt cache invalidation. See
+ * {@link PropertyInvalidatedCacheTest#testBasicCache()} for an example.
+ *
+ * </ul>
+ *
* The intended use case is caching frequently-read, seldom-changed information normally retrieved
* across interprocess communication. Imagine that you've written a user birthday information
* daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface over
@@ -136,20 +150,20 @@
* string is permitted. The third parameters is the name of the API being cached; this, too, can
* any value. The fourth is the name of the cache. The cache is usually named after th API.
* Some things you must know about the three strings:
- * <list>
- * <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}.
+ * <ul>
+ * <li> The system property that controls the cache is named {@code cache_key.<module>.<api>}.
* Usually, the SELinux rules permit a process to write a system property (and therefore
* invalidate a cache) based on the wildcard {@code cache_key.<module>.*}. This means that
* although the cache can be constructed with any module string, whatever string is chosen must be
* consistent with the SELinux configuration.
- * <ul> The API name can be any string of alphanumeric characters. All caches with the same API
+ * <li> The API name can be any string of alphanumeric characters. All caches with the same API
* are invalidated at the same time. If a server supports several caches and all are invalidated
* in common, then it is most efficient to assign the same API string to every cache.
- * <ul> The cache name can be any string. In debug output, the name is used to distiguish between
+ * <li> The cache name can be any string. In debug output, the name is used to distiguish between
* caches with the same API name. The cache name is also used when disabling caches in the
* current process. So, invalidation is based on the module+api but disabling (which is generally
* a once-per-process operation) is based on the cache name.
- * </list>
+ * </ul>
*
* User birthdays do occasionally change, so we have to modify the server to invalidate this
* cache when necessary. That invalidation code looks like this:
diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java
index cfbf528..a5697fb 100644
--- a/core/java/android/os/SystemVibratorManager.java
+++ b/core/java/android/os/SystemVibratorManager.java
@@ -16,6 +16,8 @@
package android.os;
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -138,14 +140,14 @@
Log.w(TAG, "Failed to vibrate; no vibrator manager service.");
return;
}
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason=" + reason);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "vibrate");
try {
mService.vibrate(uid, mContext.getDeviceId(), opPkg, effect, attributes, reason,
mToken);
} catch (RemoteException e) {
Log.w(TAG, "Failed to vibrate.", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -155,14 +157,14 @@
Log.w(TAG, "Failed to perform haptic feedback; no vibrator manager service.");
return;
}
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedback, reason=" + reason);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedback");
try {
mService.performHapticFeedback(mUid, mContext.getDeviceId(), mPackageName, constant,
reason, flags, privFlags);
} catch (RemoteException e) {
Log.w(TAG, "Failed to perform haptic feedback.", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -174,15 +176,14 @@
+ " no vibrator manager service.");
return;
}
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR,
- "performHapticFeedbackForInputDevice, reason=" + reason);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
try {
mService.performHapticFeedbackForInputDevice(mUid, mContext.getDeviceId(), mPackageName,
constant, inputDeviceId, inputSource, reason, flags, privFlags);
} catch (RemoteException e) {
Log.w(TAG, "Failed to perform haptic feedback for input device.", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index b8a8be1..d82af55 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -10750,6 +10750,16 @@
"lock_screen_show_only_unseen_notifications";
/**
+ * Indicates whether to minimalize the number of notifications to show on the lockscreen.
+ * <p>
+ * Type: int (0 for false, 1 for true)
+ *
+ * @hide
+ */
+ public static final String LOCK_SCREEN_NOTIFICATION_MINIMALISM =
+ "lock_screen_notification_minimalism";
+
+ /**
* Indicates whether snooze options should be shown on notifications
* <p>
* Type: int (0 for false, 1 for true)
diff --git a/core/java/android/security/OWNERS b/core/java/android/security/OWNERS
index c38ee08..325d274 100644
--- a/core/java/android/security/OWNERS
+++ b/core/java/android/security/OWNERS
@@ -10,3 +10,4 @@
per-file FileIntegrityManager.java = file:platform/system/security:/fsverity/OWNERS
per-file IFileIntegrityService.aidl = file:platform/system/security:/fsverity/OWNERS
per-file *.aconfig = victorhsieh@google.com,eranm@google.com
+per-file *responsible_apis_flags.aconfig = haok@google.com
\ No newline at end of file
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index 45e9def..5457bbe 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -24,6 +24,17 @@
}
flag {
+ name: "asm_reintroduce_grace_period"
+ namespace: "responsible_apis"
+ description: "Allow launches within the grace period for ASM apps"
+ bug: "367702727"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+
+flag {
name: "content_uri_permission_apis"
is_exported: true
namespace: "responsible_apis"
diff --git a/core/java/android/service/notification/NotificationAssistantService.java b/core/java/android/service/notification/NotificationAssistantService.java
index 88da8eb..48d7cf7 100644
--- a/core/java/android/service/notification/NotificationAssistantService.java
+++ b/core/java/android/service/notification/NotificationAssistantService.java
@@ -18,6 +18,7 @@
import static java.lang.annotation.RetentionPolicy.SOURCE;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -90,10 +91,11 @@
= "android.service.notification.NotificationAssistantService";
/**
- * Activity Action: Show notification assistant detail setting page in NAS app.
+ * Activity Action: Show notification assistant detail setting page in the NAS app.
* <p>
- * In some cases, a matching Activity may not exist, so ensure you
- * safeguard against this.
+ * To be implemented by the NAS to offer users additional customization of intelligence
+ * features. If the action is not implemented, the OS will not provide a link to it in the
+ * Settings UI.
* <p>
* Input: Nothing.
* <p>
@@ -103,6 +105,30 @@
public static final String ACTION_NOTIFICATION_ASSISTANT_DETAIL_SETTINGS =
"android.service.notification.action.NOTIFICATION_ASSISTANT_DETAIL_SETTINGS";
+ /**
+ * Activity Action: Open notification assistant feedback page in the NAS app.
+ * <p>
+ * If the NAS does not implement this page, the OS will not show any feedback calls to action in
+ * the UI.
+ * <p>
+ * Input: {@link #EXTRA_NOTIFICATION_KEY}, the {@link StatusBarNotification#getKey()} of the
+ * notification the user wants to file feedback for.
+ * <p>
+ * Output: Nothing.
+ */
+ @FlaggedApi(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS =
+ "android.service.notification.action.NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS";
+
+ /**
+ * A string extra containing the key of the notification that the user triggered feedback for.
+ *
+ * Extra for {@link #ACTION_NOTIFICATION_ASSISTANT_FEEDBACK_SETTINGS}.
+ */
+ @FlaggedApi(Flags.FLAG_NOTIFICATION_CLASSIFICATION)
+ public static final String EXTRA_NOTIFICATION_KEY
+ = "android.service.notification.extra.NOTIFICATION_KEY";
/**
* Data type: int, the feedback rating score provided by user. The score can be any integer
diff --git a/core/java/android/view/WindowManagerPolicyConstants.java b/core/java/android/view/WindowManagerPolicyConstants.java
index c88048c..1f341ca 100644
--- a/core/java/android/view/WindowManagerPolicyConstants.java
+++ b/core/java/android/view/WindowManagerPolicyConstants.java
@@ -17,6 +17,7 @@
package android.view;
import static android.os.IInputConstants.POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY;
+import static android.os.IInputConstants.POLICY_FLAG_KEY_GESTURE_TRIGGERED;
import android.annotation.IntDef;
import android.os.PowerManager;
@@ -35,6 +36,7 @@
int FLAG_VIRTUAL = 0x00000002;
int FLAG_INJECTED_FROM_ACCESSIBILITY = POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY;
+ int FLAG_KEY_GESTURE_TRIGGERED = POLICY_FLAG_KEY_GESTURE_TRIGGERED;
int FLAG_INJECTED = 0x01000000;
int FLAG_TRUSTED = 0x02000000;
int FLAG_FILTERED = 0x04000000;
diff --git a/core/java/android/view/accessibility/AccessibilityWindowInfo.java b/core/java/android/view/accessibility/AccessibilityWindowInfo.java
index c92593f..7b6e070 100644
--- a/core/java/android/view/accessibility/AccessibilityWindowInfo.java
+++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.java
@@ -880,10 +880,6 @@
* @hide
*/
public static String typeToString(int type) {
- if (Flags.enableTypeWindowControl() && type == TYPE_WINDOW_CONTROL) {
- return "TYPE_WINDOW_CONTROL";
- }
-
switch (type) {
case TYPE_APPLICATION: {
return "TYPE_APPLICATION";
@@ -903,8 +899,12 @@
case TYPE_MAGNIFICATION_OVERLAY: {
return "TYPE_MAGNIFICATION_OVERLAY";
}
- default:
+ default: {
+ if (Flags.enableTypeWindowControl() && type == TYPE_WINDOW_CONTROL) {
+ return "TYPE_WINDOW_CONTROL";
+ }
return "<UNKNOWN:" + type + ">";
+ }
}
}
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index cc5e583..fbc30ed 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -287,6 +287,13 @@
}
flag {
+ name: "enable_fully_immersive_in_desktop"
+ namespace: "lse_desktop_experience"
+ description: "Enabled the fully immersive experience from desktop"
+ bug: "359523924"
+}
+
+flag {
name: "enable_display_focus_in_shell_transitions"
namespace: "lse_desktop_experience"
description: "Creates a shell transition when display focus switches."
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 21eefef..ccaaf63 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -70,6 +70,17 @@
}
flag {
+ name: "common_surface_animator"
+ namespace: "windowing_frontend"
+ description: "A reusable surface animator for default transition"
+ bug: "326331384"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "reduce_keyguard_transitions"
namespace: "windowing_frontend"
description: "Avoid setting keyguard transitions ready unless there are no other changes"
diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
index 12d3264..032ac42 100644
--- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
+++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java
@@ -3025,6 +3025,7 @@
@Override
public PackageImpl setSplitCodePaths(@Nullable String[] splitCodePaths) {
this.splitCodePaths = splitCodePaths;
+ this.mSplits = null; // reset for paths changed
if (splitCodePaths != null) {
int size = splitCodePaths.length;
for (int index = 0; index < size; index++) {
diff --git a/core/java/com/android/internal/util/ScreenshotRequest.java b/core/java/com/android/internal/util/ScreenshotRequest.java
index c8b7def..702e5e2 100644
--- a/core/java/com/android/internal/util/ScreenshotRequest.java
+++ b/core/java/com/android/internal/util/ScreenshotRequest.java
@@ -33,6 +33,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
+import android.view.Display;
import android.view.WindowManager;
import java.util.Objects;
@@ -53,11 +54,12 @@
private final Bitmap mBitmap;
private final Rect mBoundsInScreen;
private final Insets mInsets;
+ private final int mDisplayId;
private ScreenshotRequest(
@WindowManager.ScreenshotType int type, @WindowManager.ScreenshotSource int source,
ComponentName topComponent, int taskId, int userId,
- Bitmap bitmap, Rect boundsInScreen, Insets insets) {
+ Bitmap bitmap, Rect boundsInScreen, Insets insets, int displayId) {
mType = type;
mSource = source;
mTopComponent = topComponent;
@@ -66,6 +68,7 @@
mBitmap = bitmap;
mBoundsInScreen = boundsInScreen;
mInsets = insets;
+ mDisplayId = displayId;
}
ScreenshotRequest(Parcel in) {
@@ -77,6 +80,7 @@
mBitmap = HardwareBitmapBundler.bundleToHardwareBitmap(in.readTypedObject(Bundle.CREATOR));
mBoundsInScreen = in.readTypedObject(Rect.CREATOR);
mInsets = in.readTypedObject(Insets.CREATOR);
+ mDisplayId = in.readInt();
}
@WindowManager.ScreenshotType
@@ -113,6 +117,10 @@
return mTopComponent;
}
+ public int getDisplayId() {
+ return mDisplayId;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -128,6 +136,7 @@
dest.writeTypedObject(HardwareBitmapBundler.hardwareBitmapToBundle(mBitmap), 0);
dest.writeTypedObject(mBoundsInScreen, 0);
dest.writeTypedObject(mInsets, 0);
+ dest.writeInt(mDisplayId);
}
@NonNull
@@ -161,6 +170,7 @@
private int mTaskId = INVALID_TASK_ID;
private int mUserId = USER_NULL;
private ComponentName mTopComponent;
+ private int mDisplayId = Display.INVALID_DISPLAY;
/**
* Begin building a ScreenshotRequest.
@@ -193,7 +203,7 @@
}
return new ScreenshotRequest(mType, mSource, mTopComponent, mTaskId, mUserId, mBitmap,
- mBoundsInScreen, mInsets);
+ mBoundsInScreen, mInsets, mDisplayId);
}
/**
@@ -255,6 +265,16 @@
mInsets = insets;
return this;
}
+
+ /**
+ * Set the display ID for this request.
+ *
+ * @param displayId see {@link Display}
+ */
+ public Builder setDisplayId(int displayId) {
+ mDisplayId = displayId;
+ return this;
+ }
}
/**
diff --git a/core/jni/android_util_XmlBlock.cpp b/core/jni/android_util_XmlBlock.cpp
index 5a444bb..c364451 100644
--- a/core/jni/android_util_XmlBlock.cpp
+++ b/core/jni/android_util_XmlBlock.cpp
@@ -83,7 +83,7 @@
return 0;
}
- ResXMLParser* st = new ResXMLParser(*osb);
+ ResXMLParser* st = new(std::nothrow) ResXMLParser(*osb);
if (st == NULL) {
jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
return 0;
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 07efad8..92c3906 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4457,6 +4457,11 @@
<!-- Bytes that the PinnerService will pin for WebView -->
<integer name="config_pinnerWebviewPinBytes">0</integer>
+ <!-- Maximum memory that PinnerService will pin for apps expressed
+ as a percentage of total device memory [0,100].
+ Example: 10, means 10% of total memory will be the maximum pinned memory -->
+ <integer name="config_pinnerMaxPinnedMemoryPercentage">10</integer>
+
<!-- Number of days preloaded file cache should be preserved on a device before it can be
deleted -->
<integer name="config_keepPreloadsMinDays">7</integer>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 06b36b8..76a8e92 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3467,6 +3467,7 @@
<java-symbol type="integer" name="config_pinnerHomePinBytes" />
<java-symbol type="bool" name="config_pinnerAssistantApp" />
<java-symbol type="integer" name="config_pinnerWebviewPinBytes" />
+ <java-symbol type="integer" name="config_pinnerMaxPinnedMemoryPercentage" />
<java-symbol type="string" name="config_doubleTouchGestureEnableFile" />
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index e9ce712..9821d43 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -25,6 +25,7 @@
"BinderProxyCountingTestApp/src/**/*.java",
"BinderProxyCountingTestService/src/**/*.java",
"BinderDeathRecipientHelperApp/src/**/*.java",
+ "AppThatCallsBinderMethods/src/**/*.kt",
],
visibility: ["//visibility:private"],
}
@@ -144,6 +145,7 @@
":BinderProxyCountingTestApp",
":BinderProxyCountingTestService",
":AppThatUsesAppOps",
+ ":AppThatCallsBinderMethods",
],
}
diff --git a/core/tests/coretests/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml
index b1f1e2c..05ab783 100644
--- a/core/tests/coretests/AndroidTest.xml
+++ b/core/tests/coretests/AndroidTest.xml
@@ -26,6 +26,7 @@
<option name="test-file-name" value="BinderProxyCountingTestApp.apk" />
<option name="test-file-name" value="BinderProxyCountingTestService.apk" />
<option name="test-file-name" value="AppThatUsesAppOps.apk" />
+ <option name="test-file-name" value="AppThatCallsBinderMethods.apk" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/Android.bp b/core/tests/coretests/AppThatCallsBinderMethods/Android.bp
new file mode 100644
index 0000000..dcc0d4f
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/Android.bp
@@ -0,0 +1,20 @@
+// 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.
+
+android_test_helper_app {
+ name: "AppThatCallsBinderMethods",
+ srcs: ["src/**/*.kt"],
+ platform_apis: true,
+ static_libs: ["coretests-aidl"],
+}
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml b/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml
new file mode 100644
index 0000000..b2f6d78
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.frameworks.coretests.methodcallerhelperapp">
+ <application>
+ <receiver android:name="com.android.frameworks.coretests.methodcallerhelperapp.CallMethodsReceiver"
+ android:exported="true"/>
+ </application>
+
+</manifest>
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt
new file mode 100644
index 0000000..638cc3b
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/CallMethodsReceiver.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.frameworks.coretests.methodcallerhelperapp
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+
+import com.android.frameworks.coretests.aidl.ITestInterface
+
+/**
+ * Receiver used to call methods when a binder is received
+ * {@link android.os.BinderUncaughtExceptionHandlerTest}.
+ */
+class CallMethodsReceiver : BroadcastReceiver() {
+ private val TAG = "CallMethodsReceiver"
+
+ override fun onReceive(context: Context, intent: Intent) {
+ try {
+ when (intent.getAction()) {
+ ACTION_CALL_METHOD -> intent.getExtras()!!.let {
+ Log.i(TAG, "Received ACTION_CALL_METHOD with extras: $it")
+ val iface = it.getBinder(EXTRA_BINDER)!!.let(ITestInterface.Stub::asInterface)!!
+ val name = it.getString(EXTRA_METHOD_NAME)!!
+ try {
+ when (name) {
+ "foo" -> iface.foo(5)
+ "onewayFoo" -> iface.onewayFoo(5)
+ "bar" -> iface.bar(5)
+ else -> Log.e(TAG, "Unknown method name")
+ }
+ } catch (e: Exception) {
+ // Exceptions expected
+ }
+ }
+ else -> Log.e(TAG, "Unknown action " + intent.getAction())
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception: ", e)
+ }
+ }
+}
diff --git a/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.kt
new file mode 100644
index 0000000..37c6268
--- /dev/null
+++ b/core/tests/coretests/AppThatCallsBinderMethods/src/com/android/frameworks/coretests/methodcallerhelperapp/Constants.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.frameworks.coretests.methodcallerhelperapp
+
+const val PACKAGE_NAME = "com.android.frameworks.coretests.methodcallerhelperapp"
+const val RECEIVER_NAME = "CallMethodsReceiver"
+const val ACTION_CALL_METHOD = PACKAGE_NAME + ".ACTION_CALL_METHOD"
+const val EXTRA_METHOD_NAME = PACKAGE_NAME + ".EXTRA_METHOD_NAME"
+const val EXTRA_BINDER = PACKAGE_NAME + ".EXTRA_BINDER"
diff --git a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl
new file mode 100644
index 0000000..ffcf178
--- /dev/null
+++ b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/ITestInterface.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.frameworks.coretests.aidl;
+
+/**
+ * Just an interface with a oneway, void and non-oneway method.
+ */
+interface ITestInterface {
+ // Method order matters, since we verify transaction codes
+ int foo(int a);
+ oneway void onewayFoo(int a);
+ void bar(int a);
+}
diff --git a/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt b/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt
new file mode 100644
index 0000000..791c209
--- /dev/null
+++ b/core/tests/coretests/src/android/os/BinderUncaughtExceptionHandlerTest.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os
+
+import android.content.Intent
+import android.platform.test.annotations.DisabledOnRavenwood
+import android.platform.test.annotations.Presubmit
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+
+import com.android.frameworks.coretests.aidl.ITestInterface
+import com.android.frameworks.coretests.methodcallerhelperapp.*
+
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.mockito.ArgumentMatcher
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.ArgumentMatchers.intThat
+import org.mockito.Mockito.after
+import org.mockito.Mockito.doThrow
+import org.mockito.Mockito.never
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.quality.Strictness.STRICT_STUBS
+
+private const val TIMEOUT_DURATION_MS = 2000L
+private const val FALSE_NEG_DURATION_MS = 500L
+private const val FLAG_ONEWAY = 1
+// From ITestInterface.Stub class, these values are package private
+private const val TRANSACTION_foo = 1
+private const val TRANSACTION_onewayFoo = 2
+private const val TRANSACTION_bar = 3
+
+/** Tests functionality of {@link android.os.Binder.onUnhandledException}. */
+@DisabledOnRavenwood(reason = "multi-app")
+@Presubmit
+@RunWith(AndroidJUnit4::class)
+class BinderUncaughtExceptionHandlerTest {
+
+ val mContext = InstrumentationRegistry.getInstrumentation().getTargetContext()
+
+ @Rule @JvmField val rule = MockitoJUnit.rule().strictness(STRICT_STUBS)
+
+ @Spy var mInterfaceImpl: ITestImpl = ITestImpl()
+
+ // This subclass is needed for visibility issues (via protected), since the method we are
+ // verifying lives on the boot classpath, it is not enough to be in the same package.
+ open class ITestImpl : ITestInterface.Stub() {
+ override fun onUnhandledException(code: Int, flags: Int, e: Exception?) =
+ onUnhandledExceptionVisible(code, flags, e)
+
+ public open fun onUnhandledExceptionVisible(code: Int, flags: Int, e: Exception?) {}
+
+ @Throws(RemoteException::class)
+ override open fun foo(x: Int): Int = throw UnsupportedOperationException()
+
+ @Throws(RemoteException::class)
+ override open fun onewayFoo(x: Int): Unit = throw UnsupportedOperationException()
+
+ @Throws(RemoteException::class)
+ override open fun bar(x: Int): Unit = throw UnsupportedOperationException()
+ }
+
+ class OnewayMatcher(private val isOneway: Boolean) : ArgumentMatcher<Int> {
+ override fun matches(argument: Int?) =
+ (argument!! and FLAG_ONEWAY) == if (isOneway) 1 else 0
+
+ override fun toString() = "Expected oneway: $isOneway"
+ }
+
+ @Test
+ fun testRegularMethod_ifThrowsRuntimeException_HandlerCalled() {
+ val myException = RuntimeException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+ dispatchActionCall("foo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_foo),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testRegularMethod_ifThrowsRemoteException_HandlerCalled() {
+ val myException = RemoteException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+ dispatchActionCall("foo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_foo),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testRegularMethod_ifThrowsSecurityException_HandlerNotCalled() {
+ val myException = SecurityException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).foo(anyInt())
+
+ dispatchActionCall("foo")
+
+ // No unexpected calls
+ verify(mInterfaceImpl, after(FALSE_NEG_DURATION_MS).never())
+ .onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testVoidMethod_ifThrowsRuntimeException_HandlerCalled() {
+ val myException = RuntimeException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+ dispatchActionCall("bar")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_bar),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testVoidMethod_ifThrowsRemoteException_HandlerCalled() {
+ val myException = RemoteException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+ dispatchActionCall("bar")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_bar),
+ /* flags= */ intThat(OnewayMatcher(false)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testVoidMethod_ifThrowsSecurityException_HandlerNotCalled() {
+ val myException = SecurityException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).bar(anyInt())
+
+ dispatchActionCall("bar")
+
+ // No unexpected calls
+ verify(mInterfaceImpl, after(FALSE_NEG_DURATION_MS).never())
+ .onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testOnewayMethod_ifThrowsRuntimeException_HandlerCalled() {
+ val myException = RuntimeException("Test exception")
+ doThrow(myException).doNothing().`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+ dispatchActionCall("onewayFoo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+ /* flags= */ intThat(OnewayMatcher(true)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ @Test
+ fun testOnewayMethod_ifThrowsRemoteException_HandlerCalled() {
+ val myException = RemoteException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+ dispatchActionCall("onewayFoo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+ /* flags= */ intThat(OnewayMatcher(true)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ // All exceptions are uncaught for oneway
+ @Test
+ fun testOnewayMethod_ifThrowsSecurityException_HandlerCalled() {
+ val myException = SecurityException("Test exception")
+ doThrow(myException).`when`(mInterfaceImpl).onewayFoo(anyInt())
+
+ dispatchActionCall("onewayFoo")
+
+ verify(mInterfaceImpl, timeout(TIMEOUT_DURATION_MS))
+ .onUnhandledExceptionVisible(
+ /* transactionCode = */ eq(TRANSACTION_onewayFoo),
+ /* flags= */ intThat(OnewayMatcher(true)),
+ /* exception= */ eq(myException),
+ )
+ // No unexpected calls
+ verify(mInterfaceImpl).onUnhandledExceptionVisible(anyInt(), anyInt(), any())
+ }
+
+ private fun dispatchActionCall(methodName: String) =
+ Intent(ACTION_CALL_METHOD).apply {
+ putExtras(
+ Bundle().apply {
+ putBinder(EXTRA_BINDER, mInterfaceImpl as IBinder)
+ putString(EXTRA_METHOD_NAME, methodName)
+ }
+ )
+ setClassName(PACKAGE_NAME, CallMethodsReceiver::class.java.getName())
+ }.let { mContext.sendBroadcast(it) }
+}
diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
index 005538a..d9e90fa 100644
--- a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
+++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
@@ -29,6 +29,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
+import android.widget.TextView;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -78,6 +79,16 @@
}
@Test
+ public void testNegatedDisabledFlag() {
+ assertThat(mResources.getBoolean(R.bool.bool5)).isTrue();
+ }
+
+ @Test
+ public void testNegatedEnabledFlag() {
+ assertThat(mResources.getBoolean(R.bool.bool6)).isTrue();
+ }
+
+ @Test
public void testFlagEnabledDifferentCompilationUnit() {
assertThat(mResources.getBoolean(R.bool.bool3)).isTrue();
}
@@ -94,6 +105,26 @@
}
@Test
+ public void testDirectoryEnabledFlag() {
+ assertThat(mResources.getBoolean(R.bool.bool8)).isTrue();
+ }
+
+ @Test
+ public void testDirectoryDisabledFlag() {
+ assertThat(mResources.getBoolean(R.bool.bool7)).isTrue();
+ }
+
+ @Test
+ public void testDirectoryNegatedEnabledFlag() {
+ assertThat(mResources.getBoolean(R.bool.bool9)).isTrue();
+ }
+
+ @Test
+ public void testDirectoryNegatedDisabledFlag() {
+ assertThat(mResources.getBoolean(R.bool.bool10)).isTrue();
+ }
+
+ @Test
public void testLayoutWithDisabledElements() {
LinearLayout ll = (LinearLayout) getLayoutInflater().inflate(R.layout.layout1, null);
assertThat(ll).isNotNull();
@@ -102,6 +133,24 @@
assertThat((View) ll.findViewById(R.id.text2)).isNotNull();
}
+ @Test
+ public void testEnabledFlagLayoutOverrides() {
+ LinearLayout ll = (LinearLayout) getLayoutInflater().inflate(R.layout.layout3, null);
+ assertThat(ll).isNotNull();
+ assertThat((View) ll.findViewById(R.id.text1)).isNotNull();
+ assertThat(((TextView) ll.findViewById(R.id.text1)).getText()).isEqualTo("foobar");
+ }
+
+ @Test(expected = Resources.NotFoundException.class)
+ public void testDisabledLayout() {
+ getLayoutInflater().inflate(R.layout.layout2, null);
+ }
+
+ @Test(expected = Resources.NotFoundException.class)
+ public void testDisabledDrawable() {
+ mResources.getDrawable(R.drawable.removedpng);
+ }
+
private LayoutInflater getLayoutInflater() {
ContextWrapper c = new ContextWrapper(mContext) {
private LayoutInflater mInflater;
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
index 89acbc7..0ce403e 100644
--- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotRequestTest.java
@@ -35,6 +35,7 @@
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.os.Parcel;
+import android.view.Display;
import androidx.test.runner.AndroidJUnit4;
@@ -64,10 +65,12 @@
assertNull("Bitmap was expected to be null", out.getBitmap());
assertNull("Bounds were expected to be null", out.getBoundsInScreen());
assertEquals(Insets.NONE, out.getInsets());
+ assertEquals(Display.INVALID_DISPLAY, out.getDisplayId());
}
@Test
public void testProvidedScreenshot() {
+ int displayId = 5;
Bitmap bitmap = makeHardwareBitmap(50, 50);
ScreenshotRequest in =
new ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_OTHER)
@@ -77,6 +80,7 @@
.setBitmap(bitmap)
.setBoundsOnScreen(new Rect(10, 10, 60, 60))
.setInsets(Insets.of(2, 3, 4, 5))
+ .setDisplayId(displayId)
.build();
Parcel parcel = Parcel.obtain();
@@ -92,6 +96,7 @@
assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap));
assertEquals(new Rect(10, 10, 60, 60), out.getBoundsInScreen());
assertEquals(Insets.of(2, 3, 4, 5), out.getInsets());
+ assertEquals(displayId, out.getDisplayId());
}
@Test
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index 37f0067..0896138 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -16,12 +16,11 @@
package androidx.window.common;
-import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
import static androidx.window.common.layout.CommonFoldingFeature.parseListFromString;
-import android.annotation.NonNull;
import android.content.Context;
import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateManager;
@@ -31,16 +30,23 @@
import android.util.Log;
import android.util.SparseIntArray;
+import androidx.annotation.BinderThread;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import androidx.window.common.layout.CommonFoldingFeature;
import androidx.window.common.layout.DisplayFoldFeatureCommon;
import com.android.internal.R;
+import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
@@ -55,13 +61,6 @@
private static final boolean DEBUG = false;
/**
- * Emulated device state
- * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
- * {@link CommonFoldingFeature.State} map.
- */
- private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
-
- /**
* Device state received via
* {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}.
* The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1
@@ -71,23 +70,40 @@
* "rear display". Concurrent mode for example is activated via public API and can be active in
* both the "open" and "half folded" device states.
*/
- private DeviceState mCurrentDeviceState = new DeviceState(
- new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
- "INVALID").build());
+ // TODO: b/337820752 - Add @GuardedBy("mCurrentDeviceStateLock") after flag cleanup.
+ private DeviceState mCurrentDeviceState = INVALID_DEVICE_STATE;
- private List<DeviceState> mSupportedStates;
+ /**
+ * Lock to synchronize access to {@link #mCurrentDeviceState}.
+ *
+ * <p>This lock is used to ensure thread-safety when accessing and modifying the
+ * {@link #mCurrentDeviceState} field. It is acquired by both the binder thread (if
+ * {@link Flags#wlinfoOncreate()} is enabled) and the main thread (if
+ * {@link Flags#wlinfoOncreate()} is disabled) to prevent race conditions and
+ * ensure data consistency.
+ */
+ private final Object mCurrentDeviceStateLock = new Object();
@NonNull
private final RawFoldingFeatureProducer mRawFoldSupplier;
- private final boolean mIsHalfOpenedSupported;
+ @NonNull
+ private final DeviceStateMapper mDeviceStateMapper;
- private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+ @VisibleForTesting
+ final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
+ // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the getData()
+ // implementation. See https://errorprone.info/bugpattern/GuardedBy for limitations.
+ @SuppressWarnings("GuardedBy")
+ @BinderThread // When Flags.wlinfoOncreate() is enabled.
+ @MainThread // When Flags.wlinfoOncreate() is disabled.
@Override
public void onDeviceStateChanged(@NonNull DeviceState state) {
- mCurrentDeviceState = state;
- mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
- .this::notifyFoldingFeatureChange);
+ synchronized (mCurrentDeviceStateLock) {
+ mCurrentDeviceState = state;
+ mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer.this
+ ::notifyFoldingFeatureChangeLocked);
+ }
}
};
@@ -95,41 +111,14 @@
@NonNull RawFoldingFeatureProducer rawFoldSupplier,
@NonNull DeviceStateManager deviceStateManager) {
mRawFoldSupplier = rawFoldSupplier;
- String[] deviceStatePosturePairs = context.getResources()
- .getStringArray(R.array.config_device_state_postures);
- mSupportedStates = deviceStateManager.getSupportedDeviceStates();
- boolean isHalfOpenedSupported = false;
- for (String deviceStatePosturePair : deviceStatePosturePairs) {
- String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
- if (deviceStatePostureMapping.length != 2) {
- if (DEBUG) {
- Log.e(TAG, "Malformed device state posture pair: "
- + deviceStatePosturePair);
- }
- continue;
- }
+ mDeviceStateMapper =
+ new DeviceStateMapper(context, deviceStateManager.getSupportedDeviceStates());
- int deviceState;
- int posture;
- try {
- deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
- posture = Integer.parseInt(deviceStatePostureMapping[1]);
- } catch (NumberFormatException e) {
- if (DEBUG) {
- Log.e(TAG, "Failed to parse device state or posture: "
- + deviceStatePosturePair,
- e);
- }
- continue;
- }
- isHalfOpenedSupported = isHalfOpenedSupported
- || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
- mDeviceStateToPostureMap.put(deviceState, posture);
- }
- mIsHalfOpenedSupported = isHalfOpenedSupported;
- if (mDeviceStateToPostureMap.size() > 0) {
+ if (!mDeviceStateMapper.isDeviceStateToPostureMapEmpty()) {
+ final Executor executor =
+ Flags.wlinfoOncreate() ? Runnable::run : context.getMainExecutor();
Objects.requireNonNull(deviceStateManager)
- .registerCallback(context.getMainExecutor(), mDeviceStateCallback);
+ .registerCallback(executor, mDeviceStateCallback);
}
}
@@ -137,50 +126,51 @@
* Add a callback to mCallbacks if there is no device state. This callback will be run
* once a device state is set. Otherwise,run the callback immediately.
*/
- private void runCallbackWhenValidState(@NonNull Consumer<List<CommonFoldingFeature>> callback,
- String displayFeaturesString) {
- if (isCurrentStateValid()) {
- callback.accept(calculateFoldingFeature(displayFeaturesString));
+ private void runCallbackWhenValidState(@NonNull DeviceState state,
+ @NonNull Consumer<List<CommonFoldingFeature>> callback,
+ @NonNull String displayFeaturesString) {
+ if (mDeviceStateMapper.isDeviceStateValid(state)) {
+ callback.accept(calculateFoldingFeature(state, displayFeaturesString));
} else {
// This callback will be added to mCallbacks and removed once it runs once.
- AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
+ final AcceptOnceConsumer<List<CommonFoldingFeature>> singleRunCallback =
new AcceptOnceConsumer<>(this, callback);
addDataChangedCallback(singleRunCallback);
}
}
- /**
- * Checks to find {@link DeviceStateManagerFoldingFeatureProducer#mCurrentDeviceState} in the
- * {@link DeviceStateManagerFoldingFeatureProducer#mDeviceStateToPostureMap} which was
- * initialized in the constructor of {@link DeviceStateManagerFoldingFeatureProducer}.
- * Returns a boolean value of whether the device state is valid.
- */
- private boolean isCurrentStateValid() {
- // If the device state is not found in the map, indexOfKey returns a negative number.
- return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0;
- }
-
+ // The GuardedBy analysis is intra-procedural, meaning it doesn’t consider the implementation of
+ // addDataChangedCallback(). See https://errorprone.info/bugpattern/GuardedBy for limitations.
+ @SuppressWarnings("GuardedBy")
@Override
protected void onListenersChanged() {
super.onListenersChanged();
- if (hasListeners()) {
- mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
- } else {
- mCurrentDeviceState = new DeviceState(
- new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
- "INVALID").build());
- mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
+ synchronized (mCurrentDeviceStateLock) {
+ if (hasListeners()) {
+ mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChangeLocked);
+ } else {
+ mCurrentDeviceState = INVALID_DEVICE_STATE;
+ mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChangeLocked);
+ }
+ }
+ }
+
+ @NonNull
+ private DeviceState getCurrentDeviceState() {
+ synchronized (mCurrentDeviceStateLock) {
+ return mCurrentDeviceState;
}
}
@NonNull
@Override
public Optional<List<CommonFoldingFeature>> getCurrentData() {
- Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
- if (!isCurrentStateValid()) {
+ final Optional<String> displayFeaturesString = mRawFoldSupplier.getCurrentData();
+ final DeviceState state = getCurrentDeviceState();
+ if (!mDeviceStateMapper.isDeviceStateValid(state) || displayFeaturesString.isEmpty()) {
return Optional.empty();
} else {
- return displayFeaturesString.map(this::calculateFoldingFeature);
+ return Optional.of(calculateFoldingFeature(state, displayFeaturesString.get()));
}
}
@@ -191,7 +181,7 @@
*/
@NonNull
public List<CommonFoldingFeature> getFoldsWithUnknownState() {
- Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData();
+ final Optional<String> optionalFoldingFeatureString = mRawFoldSupplier.getCurrentData();
if (optionalFoldingFeatureString.isPresent()) {
return CommonFoldingFeature.parseListFromString(
@@ -201,7 +191,6 @@
return Collections.emptyList();
}
-
/**
* Returns the list of supported {@link DisplayFoldFeatureCommon} calculated from the
* {@link DeviceStateManagerFoldingFeatureProducer}.
@@ -218,16 +207,16 @@
return foldFeatures;
}
-
/**
* Returns {@code true} if the device supports half-opened mode, {@code false} otherwise.
*/
public boolean isHalfOpenedSupported() {
- return mIsHalfOpenedSupported;
+ return mDeviceStateMapper.mIsHalfOpenedSupported;
}
/**
* Adds the data to the storeFeaturesConsumer when the data is ready.
+ *
* @param storeFeaturesConsumer a consumer to collect the data when it is first available.
*/
@Override
@@ -236,38 +225,123 @@
if (TextUtils.isEmpty(displayFeaturesString)) {
storeFeaturesConsumer.accept(new ArrayList<>());
} else {
- runCallbackWhenValidState(storeFeaturesConsumer, displayFeaturesString);
+ final DeviceState state = getCurrentDeviceState();
+ runCallbackWhenValidState(state, storeFeaturesConsumer, displayFeaturesString);
}
});
}
- private void notifyFoldingFeatureChange(String displayFeaturesString) {
- if (!isCurrentStateValid()) {
+ @GuardedBy("mCurrentDeviceStateLock")
+ private void notifyFoldingFeatureChangeLocked(String displayFeaturesString) {
+ final DeviceState state = mCurrentDeviceState;
+ if (!mDeviceStateMapper.isDeviceStateValid(state)) {
return;
}
if (TextUtils.isEmpty(displayFeaturesString)) {
notifyDataChanged(new ArrayList<>());
} else {
- notifyDataChanged(calculateFoldingFeature(displayFeaturesString));
+ notifyDataChanged(calculateFoldingFeature(state, displayFeaturesString));
}
}
- private List<CommonFoldingFeature> calculateFoldingFeature(String displayFeaturesString) {
- return parseListFromString(displayFeaturesString, currentHingeState());
- }
-
- @CommonFoldingFeature.State
- private int currentHingeState() {
+ @NonNull
+ private List<CommonFoldingFeature> calculateFoldingFeature(@NonNull DeviceState deviceState,
+ @NonNull String displayFeaturesString) {
@CommonFoldingFeature.State
- int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(),
- COMMON_STATE_UNKNOWN);
+ final int hingeState = mDeviceStateMapper.getHingeState(deviceState);
+ return parseListFromString(displayFeaturesString, hingeState);
+ }
- if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
- posture = mDeviceStateToPostureMap.get(
- DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState,
- mSupportedStates), COMMON_STATE_UNKNOWN);
+ /**
+ * Internal class to map device states to corresponding postures.
+ *
+ * <p>This class encapsulates the logic for mapping device states to postures. The mapping is
+ * immutable after initialization to ensure thread safety.
+ */
+ private static class DeviceStateMapper {
+ /**
+ * Emulated device state
+ * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
+ * {@link CommonFoldingFeature.State} map.
+ *
+ * <p>This map must be immutable after initialization to ensure thread safety, as it may be
+ * accessed from multiple threads. Modifications should only occur during object
+ * construction.
+ */
+ private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
+
+ /**
+ * The list of device states that are supported.
+ *
+ * <p>This list must be immutable after initialization to ensure thread safety.
+ */
+ @NonNull
+ private final List<DeviceState> mSupportedStates;
+
+ final boolean mIsHalfOpenedSupported;
+
+ DeviceStateMapper(@NonNull Context context, @NonNull List<DeviceState> supportedStates) {
+ mSupportedStates = supportedStates;
+
+ final String[] deviceStatePosturePairs = context.getResources()
+ .getStringArray(R.array.config_device_state_postures);
+ boolean isHalfOpenedSupported = false;
+ for (String deviceStatePosturePair : deviceStatePosturePairs) {
+ final String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
+ if (deviceStatePostureMapping.length != 2) {
+ if (DEBUG) {
+ Log.e(TAG, "Malformed device state posture pair: "
+ + deviceStatePosturePair);
+ }
+ continue;
+ }
+
+ final int deviceState;
+ final int posture;
+ try {
+ deviceState = Integer.parseInt(deviceStatePostureMapping[0]);
+ posture = Integer.parseInt(deviceStatePostureMapping[1]);
+ } catch (NumberFormatException e) {
+ if (DEBUG) {
+ Log.e(TAG, "Failed to parse device state or posture: "
+ + deviceStatePosturePair,
+ e);
+ }
+ continue;
+ }
+ isHalfOpenedSupported = isHalfOpenedSupported
+ || posture == CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
+ mDeviceStateToPostureMap.put(deviceState, posture);
+ }
+ mIsHalfOpenedSupported = isHalfOpenedSupported;
}
- return posture;
+ boolean isDeviceStateToPostureMapEmpty() {
+ return mDeviceStateToPostureMap.size() == 0;
+ }
+
+ /**
+ * Validates if the provided deviceState exists in the {@link #mDeviceStateToPostureMap}
+ * which was initialized in the constructor of {@link DeviceStateMapper}.
+ * Returns a boolean value of whether the device state is valid.
+ */
+ boolean isDeviceStateValid(@NonNull DeviceState deviceState) {
+ // If the device state is not found in the map, indexOfKey returns a negative number.
+ return mDeviceStateToPostureMap.indexOfKey(deviceState.getIdentifier()) >= 0;
+ }
+
+ @CommonFoldingFeature.State
+ int getHingeState(@NonNull DeviceState deviceState) {
+ @CommonFoldingFeature.State
+ final int posture =
+ mDeviceStateToPostureMap.get(deviceState.getIdentifier(), COMMON_STATE_UNKNOWN);
+ if (posture != CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
+ return posture;
+ }
+
+ final int baseStateIdentifier =
+ DeviceStateUtil.calculateBaseStateIdentifier(deviceState, mSupportedStates);
+ return mDeviceStateToPostureMap.get(baseStateIdentifier, COMMON_STATE_UNKNOWN);
+ }
}
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
index bd430c0..09185ee 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp
+++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
@@ -29,6 +29,7 @@
srcs: [
"**/*.java",
+ "**/*.kt",
],
static_libs: [
@@ -41,6 +42,7 @@
"androidx.test.ext.junit",
"flag-junit",
"mockito-target-extended-minus-junit4",
+ "mockito-kotlin-nodeps",
"truth",
"testables",
"platform-test-annotations",
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt
new file mode 100644
index 0000000..90887a7
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducerTest.kt
@@ -0,0 +1,341 @@
+/*
+ * 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.common
+
+import android.content.Context
+import android.content.res.Resources
+import android.hardware.devicestate.DeviceState
+import android.hardware.devicestate.DeviceStateManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.window.common.layout.CommonFoldingFeature
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_FLAT
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_HALF_OPENED
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_UNKNOWN
+import androidx.window.common.layout.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE
+import androidx.window.common.layout.DisplayFoldFeatureCommon
+import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED
+import androidx.window.common.layout.DisplayFoldFeatureCommon.DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN
+import com.android.internal.R
+import com.android.window.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+
+/**
+ * Test class for [DeviceStateManagerFoldingFeatureProducer].
+ *
+ * Build/Install/Run:
+ * atest WMJetpackUnitTests:DeviceStateManagerFoldingFeatureProducerTest
+ */
+@RunWith(AndroidJUnit4::class)
+class DeviceStateManagerFoldingFeatureProducerTest {
+ @get:Rule
+ val setFlagsRule: SetFlagsRule = SetFlagsRule()
+
+ private val mMockDeviceStateManager = mock<DeviceStateManager>()
+ private val mMockResources = mock<Resources> {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn DEVICE_STATE_POSTURES
+ }
+ private val mMockContext = mock<Context> {
+ on { resources } doReturn mMockResources
+ }
+ private val mRawFoldSupplier = mock<RawFoldingFeatureProducer> {
+ on { currentData } doReturn Optional.of(DISPLAY_FEATURES)
+ on { getData(any<Consumer<String>>()) } doAnswer { invocation ->
+ val callback = invocation.getArgument(0) as Consumer<String>
+ callback.accept(DISPLAY_FEATURES)
+ }
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_WLINFO_ONCREATE)
+ fun testRegisterCallback_whenWlinfoOncreateIsDisabled_usesMainExecutor() {
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager).registerCallback(eq(mMockContext.mainExecutor), any())
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_WLINFO_ONCREATE)
+ fun testRegisterCallback_whenWlinfoOncreateIsEnabled_usesRunnableRun() {
+ val executorCaptor = ArgumentCaptor.forClass(Executor::class.java)
+ val runnable = mock<Runnable>()
+
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager).registerCallback(executorCaptor.capture(), any())
+ executorCaptor.value.execute(runnable)
+ verify(runnable).run()
+ }
+
+ @Test
+ fun testGetCurrentData_validCurrentState_returnsFoldingFeatureWithState() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+
+ val currentData = ffp.getCurrentData()
+
+ assertThat(currentData).isPresent()
+ assertThat(currentData.get()).containsExactlyElementsIn(HALF_OPENED_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testGetCurrentData_invalidCurrentState_returnsEmptyOptionalFoldingFeature() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val currentData = ffp.getCurrentData()
+
+ assertThat(currentData).isEmpty()
+ }
+
+ @Test
+ fun testGetFoldsWithUnknownState_validFoldingFeature_returnsFoldingFeaturesWithUnknownState() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val result = ffp.getFoldsWithUnknownState()
+
+ assertThat(result).containsExactlyElementsIn(UNKNOWN_STATE_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testGetFoldsWithUnknownState_emptyFoldingFeature_returnsEmptyList() {
+ mRawFoldSupplier.stub {
+ on { currentData } doReturn Optional.empty()
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val result = ffp.getFoldsWithUnknownState()
+
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun testGetDisplayFeatures_validFoldingFeature_returnsDisplayFoldFeatures() {
+ mRawFoldSupplier.stub {
+ on { currentData } doReturn Optional.of(DISPLAY_FEATURES_HALF_OPENED_HINGE)
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ val result = ffp.displayFeatures
+
+ assertThat(result).containsExactly(
+ DisplayFoldFeatureCommon(
+ DISPLAY_FOLD_FEATURE_TYPE_SCREEN_FOLD_IN,
+ setOf(DISPLAY_FOLD_FEATURE_PROPERTY_SUPPORTS_HALF_OPENED),
+ ),
+ )
+ }
+
+ @Test
+ fun testIsHalfOpenedSupported_withHalfOpenedPostures_returnsTrue() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ assertThat(ffp.isHalfOpenedSupported).isTrue()
+ }
+
+ @Test
+ fun testIsHalfOpenedSupported_withEmptyPostures_returnsFalse() {
+ mMockResources.stub {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn emptyArray()
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ assertThat(ffp.isHalfOpenedSupported).isFalse()
+ }
+
+ @Test
+ fun testGetData_emptyDisplayFeaturesString_callsConsumerWithEmptyList() {
+ mRawFoldSupplier.stub {
+ on { getData(any<Consumer<String>>()) } doAnswer { invocation ->
+ val callback = invocation.getArgument(0) as Consumer<String>
+ callback.accept("")
+ }
+ }
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+ ffp.getData(storeFeaturesConsumer)
+
+ verify(storeFeaturesConsumer).accept(emptyList())
+ }
+
+ @Test
+ fun testGetData_validState_callsConsumerWithFoldingFeatures() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+ val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+ ffp.getData(storeFeaturesConsumer)
+
+ verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testGetData_invalidState_addsAcceptOnceConsumerToDataChangedCallback() {
+ val ffp = DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+ val storeFeaturesConsumer = mock<Consumer<List<CommonFoldingFeature>>>()
+
+ ffp.getData(storeFeaturesConsumer)
+
+ verify(storeFeaturesConsumer, never()).accept(any())
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_HALF_OPENED)
+ ffp.mDeviceStateCallback.onDeviceStateChanged(DEVICE_STATE_OPENED)
+ verify(storeFeaturesConsumer).accept(HALF_OPENED_FOLDING_FEATURES)
+ }
+
+ @Test
+ fun testDeviceStateMapper_malformedDeviceStatePosturePair_skipsPair() {
+ val malformedDeviceStatePostures = arrayOf(
+ // Missing the posture.
+ "0",
+ // Empty string.
+ "",
+ // Too many elements.
+ "0:1:2",
+ )
+ mMockResources.stub {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn
+ malformedDeviceStatePostures
+ }
+
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager, never()).registerCallback(any(), any())
+ }
+
+ @Test
+ fun testDeviceStateMapper_invalidNumberFormat_skipsPair() {
+ val invalidNumberFormatDeviceStatePostures = arrayOf("a:1", "0:b", "a:b", ":1")
+ mMockResources.stub {
+ on { getStringArray(R.array.config_device_state_postures) } doReturn
+ invalidNumberFormatDeviceStatePostures
+ }
+
+ DeviceStateManagerFoldingFeatureProducer(
+ mMockContext,
+ mRawFoldSupplier,
+ mMockDeviceStateManager,
+ )
+
+ verify(mMockDeviceStateManager, never()).registerCallback(any(), any())
+ }
+
+ companion object {
+ // Supported device states configuration.
+ private enum class SupportedDeviceStates {
+ CLOSED, HALF_OPENED, OPENED, REAR_DISPLAY, CONCURRENT;
+
+ override fun toString() = ordinal.toString()
+
+ fun toDeviceState(): DeviceState =
+ DeviceState(DeviceState.Configuration.Builder(ordinal, name).build())
+ }
+
+ // Map of supported device states supplied by DeviceStateManager to WM Jetpack posture.
+ private val DEVICE_STATE_POSTURES =
+ arrayOf(
+ "${SupportedDeviceStates.CLOSED}:$COMMON_STATE_NO_FOLDING_FEATURES",
+ "${SupportedDeviceStates.HALF_OPENED}:$COMMON_STATE_HALF_OPENED",
+ "${SupportedDeviceStates.OPENED}:$COMMON_STATE_FLAT",
+ "${SupportedDeviceStates.REAR_DISPLAY}:$COMMON_STATE_NO_FOLDING_FEATURES",
+ "${SupportedDeviceStates.CONCURRENT}:$COMMON_STATE_USE_BASE_STATE",
+ )
+ private val DEVICE_STATE_HALF_OPENED = SupportedDeviceStates.HALF_OPENED.toDeviceState()
+ private val DEVICE_STATE_OPENED = SupportedDeviceStates.OPENED.toDeviceState()
+
+ // WindowsManager Jetpack display features.
+ private val DISPLAY_FEATURES = "fold-[1104,0,1104,1848]"
+ private val DISPLAY_FEATURES_HALF_OPENED_HINGE = "$DISPLAY_FEATURES-half-opened"
+ private val HALF_OPENED_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString(
+ DISPLAY_FEATURES,
+ COMMON_STATE_HALF_OPENED,
+ )
+ private val UNKNOWN_STATE_FOLDING_FEATURES = CommonFoldingFeature.parseListFromString(
+ DISPLAY_FEATURES,
+ COMMON_STATE_UNKNOWN,
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
new file mode 100644
index 0000000..26aae2d
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/FocusTransitionListener.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared;
+
+import com.android.wm.shell.shared.annotations.ExternalThread;
+
+/**
+ * Listener to get focus-related transition callbacks.
+ */
+@ExternalThread
+public interface FocusTransitionListener {
+ /**
+ * Called when a transition changes the top, focused display.
+ */
+ void onFocusedDisplayChanged(int displayId);
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl
new file mode 100644
index 0000000..b91d5b6
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IFocusTransitionListener.aidl
@@ -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.wm.shell.shared;
+
+/**
+ * Listener interface that to get focus-related transition callbacks.
+ */
+oneway interface IFocusTransitionListener {
+
+ /**
+ * Called when a transition changes the top, focused display.
+ */
+ void onFocusedDisplayChanged(int displayId);
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
index 3256abf..02615a9 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
@@ -20,6 +20,7 @@
import android.window.RemoteTransition;
import android.window.TransitionFilter;
+import com.android.wm.shell.shared.IFocusTransitionListener;
import com.android.wm.shell.shared.IHomeTransitionListener;
/**
@@ -59,4 +60,9 @@
*/
oneway void registerRemoteForTakeover(in TransitionFilter filter,
in RemoteTransition remoteTransition) = 6;
+
+ /**
+ * Set listener that will receive callbacks about transitions involving focus switch.
+ */
+ oneway void setFocusTransitionListener(in IFocusTransitionListener listener) = 7;
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
index 6d4ab4c..2db4311 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
@@ -22,6 +22,8 @@
import com.android.wm.shell.shared.annotations.ExternalThread;
+import java.util.concurrent.Executor;
+
/**
* Interface to manage remote transitions.
*/
@@ -44,4 +46,15 @@
* Unregisters a remote transition for all operations.
*/
default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {}
+
+ /**
+ * Sets listener that will receive callbacks about transitions involving focus switch.
+ */
+ default void setFocusTransitionListener(@NonNull FocusTransitionListener listener,
+ Executor executor) {}
+
+ /**
+ * Unsets listener that will receive callbacks about transitions involving focus switch.
+ */
+ default void unsetFocusTransitionListener(@NonNull FocusTransitionListener listener) {}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index bec2ea5..4227a6e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -123,6 +123,7 @@
import com.android.wm.shell.taskview.TaskViewFactory;
import com.android.wm.shell.taskview.TaskViewFactoryController;
import com.android.wm.shell.taskview.TaskViewTransitions;
+import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.HomeTransitionObserver;
import com.android.wm.shell.transition.MixedTransitionHandler;
import com.android.wm.shell.transition.Transitions;
@@ -742,14 +743,15 @@
@ShellMainThread Handler mainHandler,
@ShellAnimationThread ShellExecutor animExecutor,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
- HomeTransitionObserver homeTransitionObserver) {
+ HomeTransitionObserver homeTransitionObserver,
+ FocusTransitionObserver focusTransitionObserver) {
if (!context.getResources().getBoolean(R.bool.config_registerShellTransitionsOnInit)) {
// TODO(b/238217847): Force override shell init if registration is disabled
shellInit = new ShellInit(mainExecutor);
}
return new Transitions(context, shellInit, shellCommandHandler, shellController, organizer,
pool, displayController, mainExecutor, mainHandler, animExecutor,
- rootTaskDisplayAreaOrganizer, homeTransitionObserver);
+ rootTaskDisplayAreaOrganizer, homeTransitionObserver, focusTransitionObserver);
}
@WMSingleton
@@ -761,6 +763,12 @@
@WMSingleton
@Provides
+ static FocusTransitionObserver provideFocusTransitionObserver() {
+ return new FocusTransitionObserver();
+ }
+
+ @WMSingleton
+ @Provides
static TaskViewTransitions provideTaskViewTransitions(Transitions transitions) {
return new TaskViewTransitions(transitions);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
new file mode 100644
index 0000000..a1a9ca9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.Choreographer;
+import android.view.SurfaceControl;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.shared.TransactionPool;
+
+import java.util.ArrayList;
+
+public class DefaultSurfaceAnimator {
+
+ /** Builds an animator for the surface and adds it to the `animations` list. */
+ static void buildSurfaceAnimation(@NonNull ArrayList<Animator> animations,
+ @NonNull Animation anim, @NonNull SurfaceControl leash,
+ @NonNull Runnable finishCallback, @NonNull TransactionPool pool,
+ @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius,
+ @Nullable Rect clipRect, boolean isActivity) {
+ final DefaultAnimationAdapter adapter = new DefaultAnimationAdapter(anim, leash,
+ position, clipRect, cornerRadius, isActivity);
+ buildSurfaceAnimation(animations, anim, finishCallback, pool, mainExecutor, adapter);
+ }
+
+ /** Builds an animator for the surface and adds it to the `animations` list. */
+ static void buildSurfaceAnimation(@NonNull ArrayList<Animator> animations,
+ @NonNull Animation anim, @NonNull Runnable finishCallback,
+ @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor,
+ @NonNull AnimationAdapter updateListener) {
+ final SurfaceControl.Transaction transaction = pool.acquire();
+ updateListener.setTransaction(transaction);
+ final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
+ // Animation length is already expected to be scaled.
+ va.overrideDurationScale(1.0f);
+ va.setDuration(anim.computeDurationHint());
+ va.addUpdateListener(updateListener);
+ va.addListener(new AnimatorListenerAdapter() {
+ // It is possible for the end/cancel to be called more than once, which may cause
+ // issues if the animating surface has already been released. Track the finished
+ // state here to skip duplicate callbacks. See b/252872225.
+ private boolean mFinished;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onFinish();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ onFinish();
+ }
+
+ private void onFinish() {
+ if (mFinished) return;
+ mFinished = true;
+ // Apply transformation of end state in case the animation is canceled.
+ if (va.getAnimatedFraction() < 1f) {
+ va.setCurrentFraction(1f);
+ }
+
+ pool.release(transaction);
+ mainExecutor.execute(() -> {
+ animations.remove(va);
+ finishCallback.run();
+ });
+ // The update listener can continue to be called after the animation has ended if
+ // end() is called manually again before the finisher removes the animation.
+ // Remove it manually here to prevent animating a released surface.
+ // See b/252872225.
+ va.removeUpdateListener(updateListener);
+ }
+ });
+ animations.add(va);
+ }
+
+ /** The animation adapter for buildSurfaceAnimation. */
+ abstract static class AnimationAdapter implements ValueAnimator.AnimatorUpdateListener {
+ @NonNull final SurfaceControl mLeash;
+ @NonNull SurfaceControl.Transaction mTransaction;
+ private Choreographer mChoreographer;
+
+ AnimationAdapter(@NonNull SurfaceControl leash) {
+ mLeash = leash;
+ }
+
+ void setTransaction(@NonNull SurfaceControl.Transaction transaction) {
+ mTransaction = transaction;
+ }
+
+ @Override
+ public void onAnimationUpdate(@NonNull ValueAnimator animator) {
+ // The finish callback in buildSurfaceAnimation will ensure that the animation ends
+ // with fraction 1.
+ final long currentPlayTime = animator.getAnimatedFraction() >= 1f
+ ? animator.getDuration()
+ : Math.min(animator.getDuration(), animator.getCurrentPlayTime());
+ applyTransformation(animator, currentPlayTime);
+ if (mChoreographer == null) {
+ mChoreographer = Choreographer.getInstance();
+ }
+ mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
+ mTransaction.apply();
+ }
+
+ abstract void applyTransformation(@NonNull ValueAnimator animator, long currentPlayTime);
+ }
+
+ private static class DefaultAnimationAdapter extends AnimationAdapter {
+ final Transformation mTransformation = new Transformation();
+ final float[] mMatrix = new float[9];
+ @NonNull final Animation mAnim;
+ @Nullable final Point mPosition;
+ @Nullable final Rect mClipRect;
+ final float mCornerRadius;
+ final boolean mIsActivity;
+
+ DefaultAnimationAdapter(@NonNull Animation anim, @NonNull SurfaceControl leash,
+ @Nullable Point position, @Nullable Rect clipRect, float cornerRadius,
+ boolean isActivity) {
+ super(leash);
+ mAnim = anim;
+ mPosition = (position != null && (position.x != 0 || position.y != 0))
+ ? position : null;
+ mClipRect = (clipRect != null && !clipRect.isEmpty()) ? clipRect : null;
+ mCornerRadius = cornerRadius;
+ mIsActivity = isActivity;
+ }
+
+ @Override
+ void applyTransformation(@NonNull ValueAnimator animator, long currentPlayTime) {
+ final Transformation transformation = mTransformation;
+ final SurfaceControl.Transaction t = mTransaction;
+ final SurfaceControl leash = mLeash;
+ transformation.clear();
+ mAnim.getTransformation(currentPlayTime, transformation);
+ if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
+ && mIsActivity && mAnim.getExtensionEdges() != 0) {
+ t.setEdgeExtensionEffect(leash, mAnim.getExtensionEdges());
+ }
+ if (mPosition != null) {
+ transformation.getMatrix().postTranslate(mPosition.x, mPosition.y);
+ }
+ t.setMatrix(leash, transformation.getMatrix(), mMatrix);
+ t.setAlpha(leash, transformation.getAlpha());
+
+ if (mClipRect != null) {
+ Rect clipRect = mClipRect;
+ final Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE);
+ if (!extensionInsets.equals(Insets.NONE)) {
+ // Clip out any overflowing edge extension.
+ clipRect = new Rect(mClipRect);
+ clipRect.inset(extensionInsets);
+ t.setCrop(leash, clipRect);
+ }
+ if (mCornerRadius > 0 && mAnim.hasRoundedCorners()) {
+ // Rounded corner can only be applied if a crop is set.
+ t.setCrop(leash, clipRect);
+ t.setCornerRadius(leash, mCornerRadius);
+ }
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index f40e0ba..d3bed59 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -826,6 +826,11 @@
@NonNull Runnable finishCallback, @NonNull TransactionPool pool,
@NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius,
@Nullable Rect clipRect, boolean isActivity) {
+ if (Flags.commonSurfaceAnimator()) {
+ DefaultSurfaceAnimator.buildSurfaceAnimation(animations, anim, leash, finishCallback,
+ pool, mainExecutor, position, cornerRadius, clipRect, isActivity);
+ return;
+ }
final SurfaceControl.Transaction transaction = pool.acquire();
final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
final Transformation transformation = new Transformation();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
new file mode 100644
index 0000000..2f5059f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/FocusTransitionObserver.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import static android.view.Display.INVALID_DISPLAY;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
+
+import static com.android.window.flags.Flags.enableDisplayFocusInShellTransitions;
+import static com.android.wm.shell.transition.Transitions.TransitionObserver;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.wm.shell.shared.FocusTransitionListener;
+import com.android.wm.shell.shared.IFocusTransitionListener;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * The {@link TransitionObserver} that observes for transitions involving focus switch.
+ * It reports transitions to callers outside of the process via {@link IFocusTransitionListener},
+ * and callers within the process via {@link FocusTransitionListener}.
+ */
+public class FocusTransitionObserver implements TransitionObserver {
+ private static final String TAG = FocusTransitionObserver.class.getSimpleName();
+
+ private IFocusTransitionListener mRemoteListener;
+ private final Map<FocusTransitionListener, Executor> mLocalListeners =
+ new HashMap<>();
+
+ private int mFocusedDisplayId = INVALID_DISPLAY;
+
+ public FocusTransitionObserver() {}
+
+ @Override
+ public void onTransitionReady(@NonNull IBinder transition,
+ @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction) {
+ final List<TransitionInfo.Change> changes = info.getChanges();
+ for (int i = changes.size() - 1; i >= 0; i--) {
+ final TransitionInfo.Change change = changes.get(i);
+ final RunningTaskInfo task = change.getTaskInfo();
+ if (task != null && task.isFocused && change.hasFlags(FLAG_MOVED_TO_TOP)) {
+ if (mFocusedDisplayId != task.displayId) {
+ mFocusedDisplayId = task.displayId;
+ notifyFocusedDisplayChanged();
+ }
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onTransitionStarting(@NonNull IBinder transition) {}
+
+ @Override
+ public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {}
+
+ @Override
+ public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {}
+
+ /**
+ * Sets the focus transition listener that receives any transitions resulting in focus switch.
+ * This is for calls from outside the Shell, within the host process.
+ *
+ */
+ public void setLocalFocusTransitionListener(FocusTransitionListener listener,
+ Executor executor) {
+ if (!enableDisplayFocusInShellTransitions()) {
+ return;
+ }
+ mLocalListeners.put(listener, executor);
+ executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId));
+ }
+
+ /**
+ * Sets the focus transition listener that receives any transitions resulting in focus switch.
+ * This is for calls from outside the Shell, within the host process.
+ *
+ */
+ public void unsetLocalFocusTransitionListener(FocusTransitionListener listener) {
+ if (!enableDisplayFocusInShellTransitions()) {
+ return;
+ }
+ mLocalListeners.remove(listener);
+ }
+
+ /**
+ * Sets the focus transition listener that receives any transitions resulting in focus switch.
+ * This is for calls from outside the host process.
+ */
+ public void setRemoteFocusTransitionListener(Transitions transitions,
+ IFocusTransitionListener listener) {
+ if (!enableDisplayFocusInShellTransitions()) {
+ return;
+ }
+ mRemoteListener = listener;
+ notifyFocusedDisplayChangedToRemote();
+ }
+
+ /**
+ * Notifies the listener that display focus has changed.
+ */
+ public void notifyFocusedDisplayChanged() {
+ notifyFocusedDisplayChangedToRemote();
+ mLocalListeners.forEach((listener, executor) ->
+ executor.execute(() -> listener.onFocusedDisplayChanged(mFocusedDisplayId)));
+ }
+
+ private void notifyFocusedDisplayChangedToRemote() {
+ if (mRemoteListener != null) {
+ try {
+ mRemoteListener.onFocusedDisplayChanged(mFocusedDisplayId);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed call notifyFocusedDisplayChangedToRemote", e);
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index d03832d..d280dcd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -87,6 +87,8 @@
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.FocusTransitionListener;
+import com.android.wm.shell.shared.IFocusTransitionListener;
import com.android.wm.shell.shared.IHomeTransitionListener;
import com.android.wm.shell.shared.IShellTransitions;
import com.android.wm.shell.shared.ShellTransitions;
@@ -103,6 +105,7 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.concurrent.Executor;
/**
* Plays transition animations. Within this player, each transition has a lifecycle.
@@ -224,6 +227,7 @@
private final ArrayList<TransitionObserver> mObservers = new ArrayList<>();
private HomeTransitionObserver mHomeTransitionObserver;
+ private FocusTransitionObserver mFocusTransitionObserver;
/** List of {@link Runnable} instances to run when the last active transition has finished. */
private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>();
@@ -309,10 +313,12 @@
@NonNull ShellExecutor mainExecutor,
@NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor,
- @NonNull HomeTransitionObserver observer) {
+ @NonNull HomeTransitionObserver homeTransitionObserver,
+ @NonNull FocusTransitionObserver focusTransitionObserver) {
this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool,
displayController, mainExecutor, mainHandler, animExecutor,
- new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), observer);
+ new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit),
+ homeTransitionObserver, focusTransitionObserver);
}
public Transitions(@NonNull Context context,
@@ -326,7 +332,8 @@
@NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor,
@NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer,
- @NonNull HomeTransitionObserver observer) {
+ @NonNull HomeTransitionObserver homeTransitionObserver,
+ @NonNull FocusTransitionObserver focusTransitionObserver) {
mOrganizer = organizer;
mContext = context;
mMainExecutor = mainExecutor;
@@ -345,7 +352,8 @@
mHandlers.add(mRemoteTransitionHandler);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
shellInit.addInitCallback(this::onInit, this);
- mHomeTransitionObserver = observer;
+ mHomeTransitionObserver = homeTransitionObserver;
+ mFocusTransitionObserver = focusTransitionObserver;
if (android.tracing.Flags.perfettoTransitionTracing()) {
mTransitionTracer = new PerfettoTransitionTracer();
@@ -384,6 +392,8 @@
mShellCommandHandler.addCommandCallback("transitions", this, this);
mShellCommandHandler.addDumpCallback(this::dump, this);
+
+ registerObserver(mFocusTransitionObserver);
}
public boolean isRegistered() {
@@ -1573,6 +1583,21 @@
mMainExecutor.execute(
() -> mRemoteTransitionHandler.removeFiltered(remoteTransition));
}
+
+ @Override
+ public void setFocusTransitionListener(FocusTransitionListener listener,
+ Executor executor) {
+ mMainExecutor.execute(() ->
+ mFocusTransitionObserver.setLocalFocusTransitionListener(listener, executor));
+
+ }
+
+ @Override
+ public void unsetFocusTransitionListener(FocusTransitionListener listener) {
+ mMainExecutor.execute(() ->
+ mFocusTransitionObserver.unsetLocalFocusTransitionListener(listener));
+
+ }
}
/**
@@ -1634,6 +1659,15 @@
}
@Override
+ public void setFocusTransitionListener(IFocusTransitionListener listener) {
+ executeRemoteCallWithTaskPermission(mTransitions, "setFocusTransitionListener",
+ (transitions) -> {
+ transitions.mFocusTransitionObserver.setRemoteFocusTransitionListener(
+ transitions, listener);
+ });
+ }
+
+ @Override
public SurfaceControl getHomeTaskOverlayContainer() {
SurfaceControl[] result = new SurfaceControl[1];
executeRemoteCallWithTaskPermission(mTransitions, "getHomeTaskOverlayContainer",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
index a2df22c..40b685c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java
@@ -17,6 +17,7 @@
package com.android.wm.shell;
import static android.view.Display.DEFAULT_DISPLAY;
+
import static org.junit.Assume.assumeTrue;
import android.content.Context;
@@ -45,6 +46,9 @@
// Disable protolog tool when running the tests from studio
ProtoLog.REQUIRE_PROTOLOGTOOL = false;
+ // Make sure ProtoLog is initialized before any logging occurs.
+ ProtoLog.init();
+
MockitoAnnotations.initMocks(this);
final Context context =
InstrumentationRegistry.getInstrumentation().getTargetContext();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index a6c16c4..67eda8b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -74,6 +74,7 @@
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.DefaultMixedHandler;
+import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.HomeTransitionObserver;
import com.android.wm.shell.transition.Transitions;
@@ -429,7 +430,8 @@
ShellInit shellInit = new ShellInit(mMainExecutor);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
mTaskOrganizer, mTransactionPool, mock(DisplayController.class), mMainExecutor,
- mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+ mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mock(FocusTransitionObserver.class));
shellInit.init();
return t;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
index 0c18229..e540322 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java
@@ -25,11 +25,14 @@
import static android.window.TransitionInfo.FLAG_SYNC;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
+import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
+import android.animation.Animator;
+import android.animation.ValueAnimator;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.os.Binder;
@@ -37,6 +40,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.view.SurfaceControl;
+import android.view.animation.AlphaAnimation;
import android.window.TransitionInfo;
import android.window.WindowContainerToken;
@@ -56,6 +60,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
+
/**
* Tests for the default animation handler that is used if no other special-purpose handler picks
* up an animation request.
@@ -187,6 +193,34 @@
}
@Test
+ public void testBuildSurfaceAnimation() {
+ final ArrayList<Animator> animators = new ArrayList<>();
+ final AlphaAnimation animation = new AlphaAnimation(0, 1);
+ final long durationMs = 500;
+ animation.setDuration(durationMs);
+ final long[] lastCurrentPlayTime = new long[1];
+ final int[] finishCount = new int[1];
+ final Runnable finishCallback = () -> finishCount[0]++;
+ DefaultSurfaceAnimator.buildSurfaceAnimation(animators, animation, finishCallback,
+ mTransactionPool, mMainExecutor,
+ new DefaultSurfaceAnimator.AnimationAdapter(mock(SurfaceControl.class)) {
+ @Override
+ void applyTransformation(ValueAnimator animator, long currentPlayTime) {
+ lastCurrentPlayTime[0] = currentPlayTime;
+ }
+ });
+ final ValueAnimator animator = (ValueAnimator) animators.get(0);
+ mAnimExecutor.execute(() -> {
+ animator.start();
+ animator.end();
+ });
+ flushHandlers();
+ assertEquals(durationMs, lastCurrentPlayTime[0]);
+ assertEquals(1f, animator.getAnimatedFraction(), 0f /* delta */);
+ assertEquals(1, finishCount[0]);
+ }
+
+ @Test
public void startAnimation_freeformOpenChange_doesntReparentTask() {
final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN)
.setTask(createTaskInfo(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
new file mode 100644
index 0000000..d37b4cf
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.transition;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager.RunningTaskInfo;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.TransitionInfo.TransitionMode;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.window.flags.Flags;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.shared.IFocusTransitionListener;
+import com.android.wm.shell.shared.TransactionPool;
+import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for the focus transition observer.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+@RequiresFlagsEnabled(Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS)
+public class FocusTransitionObserverTest extends ShellTestCase {
+
+ static final int SECONDARY_DISPLAY_ID = 1;
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+ private IFocusTransitionListener mListener;
+ private Transitions mTransition;
+ private FocusTransitionObserver mFocusTransitionObserver;
+
+ @Before
+ public void setUp() {
+ mListener = mock(IFocusTransitionListener.class);
+ when(mListener.asBinder()).thenReturn(mock(IBinder.class));
+
+ mFocusTransitionObserver = new FocusTransitionObserver();
+ mTransition =
+ new Transitions(InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ mock(ShellInit.class), mock(ShellController.class),
+ mock(ShellTaskOrganizer.class), mock(TransactionPool.class),
+ mock(DisplayController.class), new TestShellExecutor(),
+ new Handler(Looper.getMainLooper()), new TestShellExecutor(),
+ mock(HomeTransitionObserver.class),
+ mFocusTransitionObserver);
+ mFocusTransitionObserver.setRemoteFocusTransitionListener(mTransition, mListener);
+ }
+
+ @Test
+ public void testTransitionWithMovedToFrontFlagChangesDisplayFocus() throws RemoteException {
+ final IBinder binder = mock(IBinder.class);
+ final SurfaceControl.Transaction tx = mock(SurfaceControl.Transaction.class);
+
+ // Open a task on the default display, which doesn't change display focus because the
+ // default display already has it.
+ TransitionInfo info = mock(TransitionInfo.class);
+ final List<TransitionInfo.Change> changes = new ArrayList<>();
+ setupChange(changes, 123 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
+ true /* focused */);
+ when(info.getChanges()).thenReturn(changes);
+ mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+ verify(mListener, never()).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
+ clearInvocations(mListener);
+
+ // Open a new task on the secondary display and verify display focus changes to the display.
+ changes.clear();
+ setupChange(changes, 456 /* taskId */, TRANSIT_OPEN, SECONDARY_DISPLAY_ID,
+ true /* focused */);
+ when(info.getChanges()).thenReturn(changes);
+ mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+ verify(mListener, times(1)).onFocusedDisplayChanged(SECONDARY_DISPLAY_ID);
+ clearInvocations(mListener);
+
+ // Open the first task to front and verify display focus goes back to the default display.
+ changes.clear();
+ setupChange(changes, 123 /* taskId */, TRANSIT_TO_FRONT, DEFAULT_DISPLAY,
+ true /* focused */);
+ when(info.getChanges()).thenReturn(changes);
+ mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+ verify(mListener, times(1)).onFocusedDisplayChanged(DEFAULT_DISPLAY);
+ clearInvocations(mListener);
+
+ // Open another task on the default display and verify no display focus switch as it's
+ // already on the default display.
+ changes.clear();
+ setupChange(changes, 789 /* taskId */, TRANSIT_OPEN, DEFAULT_DISPLAY,
+ true /* focused */);
+ when(info.getChanges()).thenReturn(changes);
+ mFocusTransitionObserver.onTransitionReady(binder, info, tx, tx);
+ verify(mListener, never()).onFocusedDisplayChanged(DEFAULT_DISPLAY);
+ }
+
+ private void setupChange(List<TransitionInfo.Change> changes, int taskId,
+ @TransitionMode int mode, int displayId, boolean focused) {
+ TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+ RunningTaskInfo taskInfo = mock(RunningTaskInfo.class);
+ taskInfo.taskId = taskId;
+ taskInfo.isFocused = focused;
+ when(change.hasFlags(FLAG_MOVED_TO_TOP)).thenReturn(focused);
+ taskInfo.displayId = displayId;
+ when(change.getTaskInfo()).thenReturn(taskInfo);
+ when(change.getMode()).thenReturn(mode);
+ changes.add(change);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index 8f49de0..8dfdfb4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -100,7 +100,8 @@
mHomeTransitionObserver = new HomeTransitionObserver(mContext, mMainExecutor);
mTransition = new Transitions(mContext, mock(ShellInit.class), mock(ShellController.class),
mOrganizer, mTransactionPool, mDisplayController, mMainExecutor,
- mMainHandler, mAnimExecutor, mHomeTransitionObserver);
+ mMainHandler, mAnimExecutor, mHomeTransitionObserver,
+ mock(FocusTransitionObserver.class));
mHomeTransitionObserver.setHomeTransitionListener(mTransition, mListener);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index aea14b9..6cde056 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -158,7 +158,8 @@
ShellInit shellInit = mock(ShellInit.class);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
- mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+ mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mock(FocusTransitionObserver.class));
// One from Transitions, one from RootTaskDisplayAreaOrganizer
verify(shellInit).addInitCallback(any(), eq(t));
verify(shellInit).addInitCallback(any(), isA(RootTaskDisplayAreaOrganizer.class));
@@ -170,7 +171,8 @@
ShellController shellController = mock(ShellController.class);
final Transitions t = new Transitions(mContext, shellInit, shellController,
mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
- mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+ mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mock(FocusTransitionObserver.class));
shellInit.init();
verify(shellController, times(1)).addExternalInterface(
eq(ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS), any(), any());
@@ -1238,7 +1240,8 @@
final Transitions transitions =
new Transitions(mContext, shellInit, mock(ShellController.class), mOrganizer,
mTransactionPool, createTestDisplayController(), mMainExecutor,
- mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+ mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mock(FocusTransitionObserver.class));
final RecentsTransitionHandler recentsHandler =
new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions,
mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
@@ -1780,7 +1783,8 @@
ShellInit shellInit = new ShellInit(mMainExecutor);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
mOrganizer, mTransactionPool, createTestDisplayController(), mMainExecutor,
- mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
+ mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
+ mock(FocusTransitionObserver.class));
shellInit.init();
return t;
}
diff --git a/libs/appfunctions/tests/Android.bp b/libs/appfunctions/tests/Android.bp
new file mode 100644
index 0000000..6f5eff3
--- /dev/null
+++ b/libs/appfunctions/tests/Android.bp
@@ -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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "AppFunctionsSidecarTestCases",
+ team: "trendy_team_system_intelligence",
+ static_libs: [
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "androidx.core_core-ktx",
+ "com.google.android.appfunctions.sidecar.impl",
+ "junit",
+ "kotlin-test",
+ "mockito-target-extended-minus-junit4",
+ "platform-test-annotations",
+ "testables",
+ "testng",
+ "truth",
+ ],
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+}
diff --git a/libs/appfunctions/tests/AndroidManifest.xml b/libs/appfunctions/tests/AndroidManifest.xml
new file mode 100644
index 0000000..9a7d460
--- /dev/null
+++ b/libs/appfunctions/tests/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.google.android.appfunctions.sidecar.tests">
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.mock" />
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.google.android.appfunctions.sidecar.tests">
+ </instrumentation>
+</manifest>
diff --git a/libs/appfunctions/tests/AndroidTest.xml b/libs/appfunctions/tests/AndroidTest.xml
new file mode 100644
index 0000000..8251212
--- /dev/null
+++ b/libs/appfunctions/tests/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Config for AppFunctions Sidecar Tests">
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="AppFunctionsSidecarTestCases.apk" />
+ </target_preparer>
+ <option name="test-tag" value="AppFunctionsSidecarTestCases" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.google.android.appfunctions.sidecar.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt
new file mode 100644
index 0000000..1f9fddd
--- /dev/null
+++ b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.google.android.appfunctions.sidecar.tests
+
+import android.app.appfunctions.ExecuteAppFunctionRequest
+import android.app.appfunctions.ExecuteAppFunctionResponse
+import android.app.appsearch.GenericDocument
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.android.appfunctions.sidecar.SidecarConverter
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SidecarConverterTest {
+ @Test
+ fun getSidecarExecuteAppFunctionRequest_sameContents() {
+ val extras = Bundle()
+ extras.putString("extra", "value")
+ val parameters: GenericDocument =
+ GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+ .setPropertyLong("testLong", 23)
+ .build()
+ val platformRequest: ExecuteAppFunctionRequest =
+ ExecuteAppFunctionRequest.Builder("targetPkg", "targetFunctionId")
+ .setExtras(extras)
+ .setParameters(parameters)
+ .build()
+
+ val sidecarRequest = SidecarConverter.getSidecarExecuteAppFunctionRequest(platformRequest)
+
+ assertThat(sidecarRequest.targetPackageName).isEqualTo("targetPkg")
+ assertThat(sidecarRequest.functionIdentifier).isEqualTo("targetFunctionId")
+ assertThat(sidecarRequest.parameters).isEqualTo(parameters)
+ assertThat(sidecarRequest.extras.size()).isEqualTo(1)
+ assertThat(sidecarRequest.extras.getString("extra")).isEqualTo("value")
+ }
+
+ @Test
+ fun getPlatformExecuteAppFunctionRequest_sameContents() {
+ val extras = Bundle()
+ extras.putString("extra", "value")
+ val parameters: GenericDocument =
+ GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+ .setPropertyLong("testLong", 23)
+ .build()
+ val sidecarRequest =
+ com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest.Builder(
+ "targetPkg",
+ "targetFunctionId"
+ )
+ .setExtras(extras)
+ .setParameters(parameters)
+ .build()
+
+ val platformRequest = SidecarConverter.getPlatformExecuteAppFunctionRequest(sidecarRequest)
+
+ assertThat(platformRequest.targetPackageName).isEqualTo("targetPkg")
+ assertThat(platformRequest.functionIdentifier).isEqualTo("targetFunctionId")
+ assertThat(platformRequest.parameters).isEqualTo(parameters)
+ assertThat(platformRequest.extras.size()).isEqualTo(1)
+ assertThat(platformRequest.extras.getString("extra")).isEqualTo("value")
+ }
+
+ @Test
+ fun getSidecarExecuteAppFunctionResponse_successResponse_sameContents() {
+ val resultGd: GenericDocument =
+ GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+ .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true)
+ .build()
+ val platformResponse = ExecuteAppFunctionResponse.newSuccess(resultGd, null)
+
+ val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse(
+ platformResponse
+ )
+
+ assertThat(sidecarResponse.isSuccess).isTrue()
+ assertThat(
+ sidecarResponse.resultDocument.getProperty(
+ ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE
+ )
+ )
+ .isEqualTo(booleanArrayOf(true))
+ assertThat(sidecarResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK)
+ assertThat(sidecarResponse.errorMessage).isNull()
+ }
+
+ @Test
+ fun getSidecarExecuteAppFunctionResponse_errorResponse_sameContents() {
+ val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build()
+ val platformResponse =
+ ExecuteAppFunctionResponse.newFailure(
+ ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+ null,
+ null
+ )
+
+ val sidecarResponse = SidecarConverter.getSidecarExecuteAppFunctionResponse(
+ platformResponse
+ )
+
+ assertThat(sidecarResponse.isSuccess).isFalse()
+ assertThat(sidecarResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace)
+ assertThat(sidecarResponse.resultDocument.id).isEqualTo(emptyGd.id)
+ assertThat(sidecarResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType)
+ assertThat(sidecarResponse.resultCode)
+ .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR)
+ assertThat(sidecarResponse.errorMessage).isNull()
+ }
+
+ @Test
+ fun getPlatformExecuteAppFunctionResponse_successResponse_sameContents() {
+ val resultGd: GenericDocument =
+ GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "")
+ .setPropertyBoolean(ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE, true)
+ .build()
+ val sidecarResponse = com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse
+ .newSuccess(resultGd, null)
+
+ val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse(
+ sidecarResponse
+ )
+
+ assertThat(platformResponse.isSuccess).isTrue()
+ assertThat(
+ platformResponse.resultDocument.getProperty(
+ ExecuteAppFunctionResponse.PROPERTY_RETURN_VALUE
+ )
+ )
+ .isEqualTo(booleanArrayOf(true))
+ assertThat(platformResponse.resultCode).isEqualTo(ExecuteAppFunctionResponse.RESULT_OK)
+ assertThat(platformResponse.errorMessage).isNull()
+ }
+
+ @Test
+ fun getPlatformExecuteAppFunctionResponse_errorResponse_sameContents() {
+ val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build()
+ val sidecarResponse =
+ com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse.newFailure(
+ ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR,
+ null,
+ null
+ )
+
+ val platformResponse = SidecarConverter.getPlatformExecuteAppFunctionResponse(
+ sidecarResponse
+ )
+
+ assertThat(platformResponse.isSuccess).isFalse()
+ assertThat(platformResponse.resultDocument.namespace).isEqualTo(emptyGd.namespace)
+ assertThat(platformResponse.resultDocument.id).isEqualTo(emptyGd.id)
+ assertThat(platformResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType)
+ assertThat(platformResponse.resultCode)
+ .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR)
+ assertThat(platformResponse.errorMessage).isNull()
+ }
+}
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index b6476c9..ae46a99 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -50,6 +50,10 @@
constexpr bool resample_gainmap_regions() {
return false;
}
+
+constexpr bool query_global_priority() {
+ return false;
+}
} // namespace hwui_flags
#endif
@@ -110,6 +114,7 @@
bool Properties::hdr10bitPlus = false;
bool Properties::skipTelemetry = false;
bool Properties::resampleGainmapRegions = false;
+bool Properties::queryGlobalPriority = false;
int Properties::timeoutMultiplier = 1;
@@ -187,6 +192,7 @@
hdr10bitPlus = hwui_flags::hdr_10bit_plus();
resampleGainmapRegions = base::GetBoolProperty("debug.hwui.resample_gainmap_regions",
hwui_flags::resample_gainmap_regions());
+ queryGlobalPriority = hwui_flags::query_global_priority();
timeoutMultiplier = android::base::GetIntProperty("ro.hw_timeout_multiplier", 1);
skipTelemetry = base::GetBoolProperty(PROPERTY_SKIP_EGLMANAGER_TELEMETRY,
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index db47152..6f84796 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -346,6 +346,7 @@
static bool hdr10bitPlus;
static bool skipTelemetry;
static bool resampleGainmapRegions;
+ static bool queryGlobalPriority;
static int timeoutMultiplier;
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index ab052b9..93df478 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -129,3 +129,13 @@
description: "APIs that expose gainmap metadata corresponding to those defined in ISO 21496-1"
bug: "349357636"
}
+
+flag {
+ name: "query_global_priority"
+ namespace: "core_graphics"
+ description: "Attempt to query whether the vulkan driver supports the requested global priority before queue creation."
+ bug: "343986434"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index e302393..6571d92 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -44,7 +44,7 @@
namespace renderthread {
// Not all of these are strictly required, but are all enabled if present.
-static std::array<std::string_view, 21> sEnableExtensions{
+static std::array<std::string_view, 23> sEnableExtensions{
VK_KHR_BIND_MEMORY_2_EXTENSION_NAME,
VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME,
@@ -65,6 +65,8 @@
VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME,
VK_KHR_ANDROID_SURFACE_EXTENSION_NAME,
VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME,
+ VK_EXT_GLOBAL_PRIORITY_QUERY_EXTENSION_NAME,
+ VK_KHR_GLOBAL_PRIORITY_EXTENSION_NAME,
VK_EXT_DEVICE_FAULT_EXTENSION_NAME,
};
@@ -206,7 +208,7 @@
GET_INST_PROC(GetPhysicalDeviceFeatures2);
GET_INST_PROC(GetPhysicalDeviceImageFormatProperties2);
GET_INST_PROC(GetPhysicalDeviceProperties);
- GET_INST_PROC(GetPhysicalDeviceQueueFamilyProperties);
+ GET_INST_PROC(GetPhysicalDeviceQueueFamilyProperties2);
uint32_t gpuCount;
LOG_ALWAYS_FATAL_IF(mEnumeratePhysicalDevices(mInstance, &gpuCount, nullptr));
@@ -225,21 +227,30 @@
// query to get the initial queue props size
uint32_t queueCount = 0;
- mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, nullptr);
+ mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, nullptr);
LOG_ALWAYS_FATAL_IF(!queueCount);
// now get the actual queue props
- std::unique_ptr<VkQueueFamilyProperties[]> queueProps(new VkQueueFamilyProperties[queueCount]);
- mGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueCount, queueProps.get());
+ std::unique_ptr<VkQueueFamilyProperties2[]>
+ queueProps(new VkQueueFamilyProperties2[queueCount]);
+ // query the global priority, this ignored if VK_EXT_global_priority isn't supported
+ std::vector<VkQueueFamilyGlobalPriorityPropertiesEXT> queuePriorityProps(queueCount);
+ for (uint32_t i = 0; i < queueCount; i++) {
+ queuePriorityProps[i].sType = VK_STRUCTURE_TYPE_QUEUE_FAMILY_GLOBAL_PRIORITY_PROPERTIES_EXT;
+ queuePriorityProps[i].pNext = nullptr;
+ queueProps[i].pNext = &queuePriorityProps[i];
+ }
+ mGetPhysicalDeviceQueueFamilyProperties2(mPhysicalDevice, &queueCount, queueProps.get());
constexpr auto kRequestedQueueCount = 2;
// iterate to find the graphics queue
mGraphicsQueueIndex = queueCount;
for (uint32_t i = 0; i < queueCount; i++) {
- if (queueProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
+ if (queueProps[i].queueFamilyProperties.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
mGraphicsQueueIndex = i;
- LOG_ALWAYS_FATAL_IF(queueProps[i].queueCount < kRequestedQueueCount);
+ LOG_ALWAYS_FATAL_IF(
+ queueProps[i].queueFamilyProperties.queueCount < kRequestedQueueCount);
break;
}
}
@@ -327,6 +338,15 @@
tailPNext = &formatFeatures->pNext;
}
+ VkPhysicalDeviceGlobalPriorityQueryFeaturesEXT* globalPriorityQueryFeatures =
+ new VkPhysicalDeviceGlobalPriorityQueryFeaturesEXT;
+ globalPriorityQueryFeatures->sType =
+ VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GLOBAL_PRIORITY_QUERY_FEATURES_EXT;
+ globalPriorityQueryFeatures->pNext = nullptr;
+ globalPriorityQueryFeatures->globalPriorityQuery = false;
+ *tailPNext = globalPriorityQueryFeatures;
+ tailPNext = &globalPriorityQueryFeatures->pNext;
+
// query to get the physical device features
mGetPhysicalDeviceFeatures2(mPhysicalDevice, &features);
// this looks like it would slow things down,
@@ -341,24 +361,59 @@
if (Properties::contextPriority != 0 &&
grExtensions.hasExtension(VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME, 2)) {
- memset(&queuePriorityCreateInfo, 0, sizeof(VkDeviceQueueGlobalPriorityCreateInfoEXT));
- queuePriorityCreateInfo.sType =
- VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_EXT;
- queuePriorityCreateInfo.pNext = nullptr;
+ VkQueueGlobalPriorityEXT globalPriority;
switch (Properties::contextPriority) {
case EGL_CONTEXT_PRIORITY_LOW_IMG:
- queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_LOW_EXT;
+ globalPriority = VK_QUEUE_GLOBAL_PRIORITY_LOW_EXT;
break;
case EGL_CONTEXT_PRIORITY_MEDIUM_IMG:
- queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_MEDIUM_EXT;
+ globalPriority = VK_QUEUE_GLOBAL_PRIORITY_MEDIUM_EXT;
break;
case EGL_CONTEXT_PRIORITY_HIGH_IMG:
- queuePriorityCreateInfo.globalPriority = VK_QUEUE_GLOBAL_PRIORITY_HIGH_EXT;
+ globalPriority = VK_QUEUE_GLOBAL_PRIORITY_HIGH_EXT;
break;
default:
LOG_ALWAYS_FATAL("Unsupported context priority");
}
- queueNextPtr = &queuePriorityCreateInfo;
+
+ // check if the requested priority is reported by the query
+ bool attachGlobalPriority = false;
+ if (uirenderer::Properties::queryGlobalPriority &&
+ globalPriorityQueryFeatures->globalPriorityQuery) {
+ for (uint32_t i = 0; i < queuePriorityProps[mGraphicsQueueIndex].priorityCount; i++) {
+ if (queuePriorityProps[mGraphicsQueueIndex].priorities[i] == globalPriority) {
+ attachGlobalPriority = true;
+ break;
+ }
+ }
+ } else {
+ // Querying is not supported so attempt queue creation with requested priority anyways
+ // If the priority turns out not to be supported, the driver *may* fail with
+ // VK_ERROR_NOT_PERMITTED_KHR
+ attachGlobalPriority = true;
+ }
+
+ if (attachGlobalPriority) {
+ memset(&queuePriorityCreateInfo, 0, sizeof(VkDeviceQueueGlobalPriorityCreateInfoEXT));
+ queuePriorityCreateInfo.sType =
+ VK_STRUCTURE_TYPE_DEVICE_QUEUE_GLOBAL_PRIORITY_CREATE_INFO_EXT;
+ queuePriorityCreateInfo.pNext = nullptr;
+ queuePriorityCreateInfo.globalPriority = globalPriority;
+ queueNextPtr = &queuePriorityCreateInfo;
+ } else {
+ // If globalPriorityQuery is enabled, attempting queue creation with an unsupported
+ // priority will return VK_ERROR_INITIALIZATION_FAILED.
+ //
+ // SysUI and Launcher will request HIGH when SF has RT but it is a known issue that
+ // upstream drm drivers currently lack a way to grant them the granular privileges
+ // they need for HIGH (but not RT) so they will fail queue creation.
+ // For now, drop the unsupported global priority request so that queue creation
+ // succeeds.
+ //
+ // Once that is fixed, this should probably be a fatal error indicating an improper
+ // request or an app needs to get the correct privileges.
+ ALOGW("Requested context priority is not supported by the queue");
+ }
}
const VkDeviceQueueCreateInfo queueInfo = {
diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h
index f042571..a593ec6 100644
--- a/libs/hwui/renderthread/VulkanManager.h
+++ b/libs/hwui/renderthread/VulkanManager.h
@@ -152,7 +152,7 @@
VkPtr<PFN_vkDestroyInstance> mDestroyInstance;
VkPtr<PFN_vkEnumeratePhysicalDevices> mEnumeratePhysicalDevices;
VkPtr<PFN_vkGetPhysicalDeviceProperties> mGetPhysicalDeviceProperties;
- VkPtr<PFN_vkGetPhysicalDeviceQueueFamilyProperties> mGetPhysicalDeviceQueueFamilyProperties;
+ VkPtr<PFN_vkGetPhysicalDeviceQueueFamilyProperties2> mGetPhysicalDeviceQueueFamilyProperties2;
VkPtr<PFN_vkGetPhysicalDeviceFeatures2> mGetPhysicalDeviceFeatures2;
VkPtr<PFN_vkGetPhysicalDeviceImageFormatProperties2> mGetPhysicalDeviceImageFormatProperties2;
VkPtr<PFN_vkCreateDevice> mCreateDevice;
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 31f8996..ef4c3ef 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -16,6 +16,8 @@
package android.media.projection;
+import static android.view.Display.DEFAULT_DISPLAY;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.compat.CompatChanges;
@@ -29,6 +31,7 @@
import android.os.Build;
import android.os.Handler;
import android.os.RemoteException;
+import android.os.UserManager;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
@@ -70,6 +73,7 @@
private final DisplayManager mDisplayManager;
@NonNull
private final Map<Callback, CallbackRecord> mCallbacks = new ArrayMap<>();
+ private final int mDisplayId;
/** @hide */
public MediaProjection(Context context, IMediaProjection impl) {
@@ -88,6 +92,11 @@
throw new RuntimeException("Failed to start media projection", e);
}
mDisplayManager = displayManager;
+
+ final UserManager userManager = context.getSystemService(UserManager.class);
+ mDisplayId = userManager.isVisibleBackgroundUsersSupported()
+ ? userManager.getMainDisplayIdAssignedToUser()
+ : DEFAULT_DISPLAY;
}
/**
@@ -156,6 +165,7 @@
if (surface != null) {
builder.setSurface(surface);
}
+ builder.setDisplayIdToMirror(mDisplayId);
return createVirtualDisplay(builder, callback, handler);
}
@@ -234,6 +244,7 @@
if (surface != null) {
builder.setSurface(surface);
}
+ builder.setDisplayIdToMirror(mDisplayId);
return createVirtualDisplay(builder, callback, handler);
}
diff --git a/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java b/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java
index c376f25..8b28347 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerInstrumentation.java
@@ -17,6 +17,7 @@
package android.media.soundtrigger;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -328,6 +329,7 @@
* Get the recognition config used to start this recognition.
* @return - The config passed to the HAL for startRecognition.
*/
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
public @NonNull SoundTrigger.RecognitionConfig getRecognitionConfig() {
return mRecognitionConfig;
}
diff --git a/media/java/android/media/soundtrigger/SoundTriggerManager.java b/media/java/android/media/soundtrigger/SoundTriggerManager.java
index 78352871..3d0c406 100644
--- a/media/java/android/media/soundtrigger/SoundTriggerManager.java
+++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java
@@ -19,6 +19,7 @@
import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -42,7 +43,6 @@
import android.media.permission.Identity;
import android.media.permission.SafeCloseable;
import android.os.Binder;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -463,13 +463,18 @@
public static final String EXTRA_STATUS = "android.media.soundtrigger.STATUS";
/**
- * Loads a given sound model into the sound trigger. Note the model will be unloaded if there is
- * an error/the system service is restarted.
- * @hide
+ * Loads a given sound model into the sound trigger.
+ *
+ * <p><b>Note:</b> the model will be unloaded if there is an error/the system service is
+ * restarted.
+ *
+ * @return {@link SoundTrigger#STATUS_OK} if the model was loaded successfully, error code
+ * otherwise
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
@UnsupportedAppUsage
- @TestApi
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
public int loadSoundModel(@NonNull SoundModel soundModel) {
if (mSoundTriggerSession == null) {
throw new IllegalStateException("No underlying SoundTriggerModule available");
@@ -508,11 +513,11 @@
*
* @return {@link SoundTrigger#STATUS_OK} if the recognition could be started, error code
* otherwise
- *
- * @hide
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
@UnsupportedAppUsage
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
public int startRecognition(@NonNull UUID soundModelId, @Nullable Bundle params,
@NonNull ComponentName detectionService, @NonNull RecognitionConfig config) {
Objects.requireNonNull(soundModelId);
@@ -531,11 +536,15 @@
/**
* Stops the given model's recognition.
- * @hide
+ *
+ * @return {@link SoundTrigger#STATUS_OK} if the recognition could be stopped, error code
+ * otherwise
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public int stopRecognition(UUID soundModelId) {
+ @UnsupportedAppUsage
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
+ public int stopRecognition(@NonNull UUID soundModelId) {
if (mSoundTriggerSession == null) {
throw new IllegalStateException("No underlying SoundTriggerModule available");
}
@@ -548,12 +557,18 @@
}
/**
- * Removes the given model from memory. Will also stop any pending recognitions.
- * @hide
+ * Removes the given model from memory.
+ *
+ * <p>Will also stop any pending recognitions.
+ *
+ * @return {@link SoundTrigger#STATUS_OK} if the model was unloaded successfully, error code
+ * otherwise
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
- @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public int unloadSoundModel(UUID soundModelId) {
+ @UnsupportedAppUsage
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
+ public int unloadSoundModel(@NonNull UUID soundModelId) {
if (mSoundTriggerSession == null) {
throw new IllegalStateException("No underlying SoundTriggerModule available");
}
@@ -566,12 +581,13 @@
}
/**
- * Returns true if the given model has had detection started on it.
- * @hide
+ * Returns whether the given model has had detection started on it.
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
@UnsupportedAppUsage
- public boolean isRecognitionActive(UUID soundModelId) {
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
+ public boolean isRecognitionActive(@NonNull UUID soundModelId) {
if (soundModelId == null || mSoundTriggerSession == null) {
return false;
}
@@ -599,14 +615,16 @@
}
/**
- * Asynchronously get state of the indicated model. The model state is returned as
- * a recognition event in the callback that was registered in the startRecognition
- * method.
- * @hide
+ * Asynchronously gets state of the indicated model.
+ *
+ * <p>The model state is returned as a recognition event in the callback that was registered
+ * in the {@link #startRecognition(UUID, Bundle, ComponentName, RecognitionConfig)} method.
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
@UnsupportedAppUsage
- public int getModelState(UUID soundModelId) {
+ @FlaggedApi(Flags.FLAG_MANAGER_API)
+ public int getModelState(@NonNull UUID soundModelId) {
if (mSoundTriggerSession == null) {
throw new IllegalStateException("No underlying SoundTriggerModule available");
}
@@ -621,7 +639,7 @@
}
/**
- * Get the hardware sound trigger module properties currently loaded.
+ * Gets the hardware sound trigger module properties currently loaded.
*
* @return The properties currently loaded. Returns null if no supported hardware loaded.
*/
@@ -639,9 +657,11 @@
}
/**
- * Set a model specific {@link ModelParams} with the given value. This
- * parameter will keep its value for the duration the model is loaded regardless of starting and
- * stopping recognition. Once the model is unloaded, the value will be lost.
+ * Sets a model specific {@link ModelParams} with the given value.
+ *
+ * <p>This parameter will keep its value for the duration the model is loaded regardless of
+ * starting and stopping recognition. Once the model is unloaded, the value will be lost.
+ *
* {@link SoundTriggerManager#queryParameter} should be checked first before calling this
* method.
*
@@ -671,12 +691,15 @@
}
/**
- * Get a model specific {@link ModelParams}. This parameter will keep its value
- * for the duration the model is loaded regardless of starting and stopping recognition.
- * Once the model is unloaded, the value will be lost. If the value is not set, a default
- * value is returned. See {@link ModelParams} for parameter default values.
- * {@link SoundTriggerManager#queryParameter} should be checked first before
- * calling this method. Otherwise, an exception can be thrown.
+ * Gets a model specific {@link ModelParams}.
+ *
+ * <p>This parameter will keep its value for the duration the model is loaded regardless
+ * of starting and stopping recognition. Once the model is unloaded, the value will be lost.
+ * If the value is not set, a default value is returned. See {@link ModelParams} for
+ * parameter default values.
+ *
+ * {@link SoundTriggerManager#queryParameter} should be checked first before calling this
+ * method. Otherwise, an exception can be thrown.
*
* @param soundModelId UUID of model to get parameter
* @param modelParam {@link ModelParams}
@@ -697,9 +720,10 @@
}
/**
- * Determine if parameter control is supported for the given model handle.
- * This method should be checked prior to calling {@link SoundTriggerManager#setParameter} or
- * {@link SoundTriggerManager#getParameter}.
+ * Determines if parameter control is supported for the given model handle.
+ *
+ * <p>This method should be checked prior to calling {@link SoundTriggerManager#setParameter}
+ * or {@link SoundTriggerManager#getParameter}.
*
* @param soundModelId handle of model to get parameter
* @param modelParam {@link ModelParams}
diff --git a/packages/SettingsLib/ActionButtonsPreference/Android.bp b/packages/SettingsLib/ActionButtonsPreference/Android.bp
index 71ecb4c..37a0e79 100644
--- a/packages/SettingsLib/ActionButtonsPreference/Android.bp
+++ b/packages/SettingsLib/ActionButtonsPreference/Android.bp
@@ -19,6 +19,7 @@
static_libs: [
"androidx.preference_preference",
+ "SettingsLibSettingsTheme",
],
sdk_version: "system_current",
diff --git a/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml
new file mode 100644
index 0000000..3e73ebd
--- /dev/null
+++ b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/settingslib_expressive_space_extrasmall4"
+ android:paddingHorizontal="@dimen/settingslib_expressive_space_extrasmall4"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:id="@+id/action1"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button1"
+ style="@style/SettingsLibActionButton.Expressive"
+ android:layout_width="@dimen/settingslib_expressive_space_large3"
+ android:layout_height="@dimen/settingslib_expressive_space_medium5"
+ android:layout_gravity="center_horizontal"
+ android:importantForAccessibility="no"/>
+ <TextView
+ android:id="@+id/text1"
+ style="@style/SettingsLibActionButton.Expressive.Label"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+ android:importantForAccessibility="no"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/action2"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button2"
+ style="@style/SettingsLibActionButton.Expressive"
+ android:layout_width="@dimen/settingslib_expressive_space_large3"
+ android:layout_height="@dimen/settingslib_expressive_space_medium5"
+ android:layout_gravity="center_horizontal"
+ android:importantForAccessibility="no"/>
+ <TextView
+ android:id="@+id/text2"
+ style="@style/SettingsLibActionButton.Expressive.Label"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+ android:importantForAccessibility="no"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/action3"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button3"
+ style="@style/SettingsLibActionButton.Expressive"
+ android:layout_width="@dimen/settingslib_expressive_space_large3"
+ android:layout_height="@dimen/settingslib_expressive_space_medium5"
+ android:layout_gravity="center_horizontal"
+ android:importantForAccessibility="no"/>
+ <TextView
+ android:id="@+id/text3"
+ style="@style/SettingsLibActionButton.Expressive.Label"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+ android:importantForAccessibility="no"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/action4"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="vertical">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/button4"
+ style="@style/SettingsLibActionButton.Expressive"
+ android:layout_width="@dimen/settingslib_expressive_space_large3"
+ android:layout_height="@dimen/settingslib_expressive_space_medium5"
+ android:layout_gravity="center_horizontal"
+ android:importantForAccessibility="no"/>
+ <TextView
+ android:id="@+id/text4"
+ style="@style/SettingsLibActionButton.Expressive.Label"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"
+ android:importantForAccessibility="no"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml b/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml
new file mode 100644
index 0000000..cc948a6
--- /dev/null
+++ b/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <style name="SettingsLibActionButton.Expressive" parent="SettingsLibButtonStyle.Expressive.Tonal">
+ <item name="android:backgroundTint">@color/settingslib_materialColorPrimaryContainer</item>
+ <item name="iconTint">@color/settingslib_materialColorOnPrimaryContainer</item>
+ <item name="iconGravity">textTop</item>
+ </style>
+
+ <style name="SettingsLibActionButton.Expressive.Label" parent="SettingsLibTextAppearance.Emphasized.Title.Small">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:minWidth">@dimen/settingslib_expressive_space_small3</item>
+ <item name="android:minHeight">@dimen/settingslib_expressive_space_small3</item>
+ <item name="android:textColor">@color/settingslib_materialColorOnSurface</item>
+ <item name="android:layout_gravity">center</item>
+ </style>
+
+</resources>
diff --git a/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java b/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
index 5dc11cf..b286182 100644
--- a/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
+++ b/packages/SettingsLib/ActionButtonsPreference/src/com/android/settingslib/widget/ActionButtonsPreference.java
@@ -26,6 +26,8 @@
import android.util.Log;
import android.view.View;
import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
@@ -34,6 +36,8 @@
import com.android.settingslib.widget.preference.actionbuttons.R;
+import com.google.android.material.button.MaterialButton;
+
import java.util.ArrayList;
import java.util.List;
@@ -98,7 +102,10 @@
}
private void init() {
- setLayoutResource(R.layout.settingslib_action_buttons);
+ int resId = SettingsThemeHelper.isExpressiveTheme(getContext())
+ ? R.layout.settingslib_expressive_action_buttons
+ : R.layout.settingslib_action_buttons;
+ setLayoutResource(resId);
setSelectable(false);
final Resources res = getContext().getResources();
@@ -127,6 +134,21 @@
mButton3Info.mButton = (Button) holder.findViewById(R.id.button3);
mButton4Info.mButton = (Button) holder.findViewById(R.id.button4);
+ if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ mButton1Info.mIsExpressive = true;
+ mButton1Info.mTextView = (TextView) holder.findViewById(R.id.text1);
+ mButton1Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action1);
+ mButton2Info.mIsExpressive = true;
+ mButton2Info.mTextView = (TextView) holder.findViewById(R.id.text2);
+ mButton2Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action2);
+ mButton3Info.mIsExpressive = true;
+ mButton3Info.mTextView = (TextView) holder.findViewById(R.id.text3);
+ mButton3Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action3);
+ mButton4Info.mIsExpressive = true;
+ mButton4Info.mTextView = (TextView) holder.findViewById(R.id.text4);
+ mButton4Info.mActionLayout = (LinearLayout) holder.findViewById(R.id.action4);
+ }
+
mDivider1 = holder.findViewById(R.id.divider1);
mDivider2 = holder.findViewById(R.id.divider2);
mDivider3 = holder.findViewById(R.id.divider3);
@@ -169,45 +191,47 @@
mVisibleButtonInfos.add(mButton4Info);
}
- final boolean isRtl = getContext().getResources().getConfiguration()
- .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
- switch (mVisibleButtonInfos.size()) {
- case SINGLE_BUTTON_STYLE :
- if (isRtl) {
- setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
- } else {
- setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
- }
- break;
- case TWO_BUTTONS_STYLE :
- if (isRtl) {
- setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
- } else {
- setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
- }
- break;
- case THREE_BUTTONS_STYLE :
- if (isRtl) {
- setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
- } else {
- setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
- }
- break;
- case FOUR_BUTTONS_STYLE :
- if (isRtl) {
- setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
- } else {
- setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
- }
- break;
- default:
- Log.e(TAG, "No visible buttons info, skip background settings.");
- break;
- }
+ if (!SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ final boolean isRtl = getContext().getResources().getConfiguration()
+ .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ switch (mVisibleButtonInfos.size()) {
+ case SINGLE_BUTTON_STYLE :
+ if (isRtl) {
+ setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
+ } else {
+ setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1);
+ }
+ break;
+ case TWO_BUTTONS_STYLE :
+ if (isRtl) {
+ setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
+ } else {
+ setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2);
+ }
+ break;
+ case THREE_BUTTONS_STYLE :
+ if (isRtl) {
+ setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
+ } else {
+ setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3);
+ }
+ break;
+ case FOUR_BUTTONS_STYLE :
+ if (isRtl) {
+ setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
+ } else {
+ setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4);
+ }
+ break;
+ default:
+ Log.e(TAG, "No visible buttons info, skip background settings.");
+ break;
+ }
- setupDivider1();
- setupDivider2();
- setupDivider3();
+ setupDivider1();
+ setupDivider2();
+ setupDivider3();
+ }
}
private void setupBackgrounds(
@@ -509,23 +533,45 @@
static class ButtonInfo {
private Button mButton;
+ private TextView mTextView;
+ private LinearLayout mActionLayout;
private CharSequence mText;
private Drawable mIcon;
private View.OnClickListener mListener;
private boolean mIsEnabled = true;
private boolean mIsVisible = true;
+ private boolean mIsExpressive = false;
void setUpButton() {
- mButton.setText(mText);
- mButton.setOnClickListener(mListener);
- mButton.setEnabled(mIsEnabled);
- mButton.setCompoundDrawablesWithIntrinsicBounds(
- null /* left */, mIcon /* top */, null /* right */, null /* bottom */);
+ if (mIsExpressive) {
+ mTextView.setText(mText);
+ if (mButton instanceof MaterialButton) {
+ ((MaterialButton) mButton).setIcon(mIcon);
+ }
+ mButton.setEnabled(mIsEnabled);
+ mActionLayout.setOnClickListener(mListener);
+ mActionLayout.setEnabled(mIsEnabled);
+ mActionLayout.setContentDescription(mText);
+ } else {
+ mButton.setText(mText);
+ mButton.setCompoundDrawablesWithIntrinsicBounds(
+ null /* left */, mIcon /* top */, null /* right */, null /* bottom */);
+ mButton.setOnClickListener(mListener);
+ mButton.setEnabled(mIsEnabled);
+ }
if (shouldBeVisible()) {
mButton.setVisibility(View.VISIBLE);
+ if (mIsExpressive) {
+ mTextView.setVisibility(View.VISIBLE);
+ mActionLayout.setVisibility(View.VISIBLE);
+ }
} else {
mButton.setVisibility(View.GONE);
+ if (mIsExpressive) {
+ mTextView.setVisibility(View.GONE);
+ mActionLayout.setVisibility(View.GONE);
+ }
}
}
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index 0cb85d8..af07686 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -32,6 +32,8 @@
"SettingsLibBannerMessagePreference",
"SettingsLibBarChartPreference",
"SettingsLibButtonPreference",
+ "SettingsLibBulletPreference",
+ "SettingsLibCardPreference",
"SettingsLibCollapsingToolbarBaseActivity",
"SettingsLibDeviceStateRotationLock",
"SettingsLibDisplayUtils",
@@ -53,6 +55,7 @@
"SettingsLibTwoTargetPreference",
"SettingsLibUsageProgressBarPreference",
"SettingsLibUtils",
+ "SettingsLibZeroStatePreference",
"settingslib_media_flags_lib",
"settingslib_flags_lib",
],
diff --git a/packages/SettingsLib/BulletPreference/Android.bp b/packages/SettingsLib/BulletPreference/Android.bp
new file mode 100644
index 0000000..3ea0b2b
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/Android.bp
@@ -0,0 +1,33 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+ name: "SettingsLibBulletPreference",
+ use_resource_processor: true,
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ resource_dirs: ["res"],
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.preference_preference",
+ "SettingsLibSettingsTheme",
+ ],
+ sdk_version: "system_current",
+ min_sdk_version: "21",
+ apex_available: [
+ "//apex_available:platform",
+ ],
+}
diff --git a/packages/SettingsLib/BulletPreference/AndroidManifest.xml b/packages/SettingsLib/BulletPreference/AndroidManifest.xml
new file mode 100644
index 0000000..c7495ef
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.widget.preference.bullet">
+
+ <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml
new file mode 100644
index 0000000..030f024
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_icon_frame.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="@dimen/settingslib_expressive_space_medium4"
+ android:gravity="top|start"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_small1"
+ android:paddingStart="@dimen/settingslib_expressive_space_extrasmall4"
+ android:paddingEnd="@dimen/settingslib_expressive_space_small1">
+
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:maxWidth="@dimen/settingslib_expressive_space_small4"
+ app:maxHeight="@dimen/settingslib_expressive_space_small4"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml
new file mode 100644
index 0000000..3f37f6c
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/res/layout/settingslib_expressive_bullet_preference.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:background="?android:attr/selectableItemBackground"
+ android:clipToPadding="false"
+ android:baselineAligned="false">
+
+ <include layout="@layout/settingslib_expressive_bullet_icon_frame"/>
+
+ <include layout="@layout/settingslib_expressive_preference_text_frame"/>
+
+ <!-- Preference should place its actual preference widget here. -->
+ <LinearLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="end|center_vertical"
+ android:paddingStart="@dimen/settingslib_expressive_space_small1"
+ android:paddingEnd="0dp"
+ android:orientation="vertical"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.kt b/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.kt
new file mode 100644
index 0000000..45e2e9a
--- /dev/null
+++ b/packages/SettingsLib/BulletPreference/src/com/android/settingslib/widget/BulletPreference.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+
+import com.android.settingslib.widget.preference.bullet.R
+
+/**
+ * The BulletPreference shows a text which describe a feature.
+ */
+class BulletPreference @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+ init {
+ layoutResource = R.layout.settingslib_expressive_bullet_preference
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+ holder.isDividerAllowedAbove = false
+ holder.isDividerAllowedBelow = false
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/CardPreference/Android.bp b/packages/SettingsLib/CardPreference/Android.bp
new file mode 100644
index 0000000..1d871d1
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/Android.bp
@@ -0,0 +1,33 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+ name: "SettingsLibCardPreference",
+ use_resource_processor: true,
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ resource_dirs: ["res"],
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.preference_preference",
+ "SettingsLibSettingsTheme",
+ ],
+ sdk_version: "system_current",
+ min_sdk_version: "21",
+ apex_available: [
+ "//apex_available:platform",
+ ],
+}
diff --git a/packages/SettingsLib/CardPreference/AndroidManifest.xml b/packages/SettingsLib/CardPreference/AndroidManifest.xml
new file mode 100644
index 0000000..717f66e
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.widget.preference.card">
+
+ <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml
new file mode 100644
index 0000000..716ed41
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/res/layout/settingslib_expressive_preference_card.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.google.android.material.card.MaterialCardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/SettingsLibCardStyle">
+
+ <LinearLayout
+ android:id="@+id/card_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
+ android:minHeight="@dimen/settingslib_expressive_space_large3"
+ android:paddingStart="@dimen/settingslib_expressive_space_small1"
+ android:paddingEnd="@dimen/settingslib_expressive_space_small1"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <LinearLayout
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minWidth="@dimen/settingslib_expressive_space_medium3"
+ android:minHeight="@dimen/settingslib_expressive_space_medium3"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:src="@drawable/settingslib_arrow_drop_down"
+ android:layout_width="@dimen/settingslib_expressive_space_medium3"
+ android:layout_height="@dimen/settingslib_expressive_space_medium3"
+ android:scaleType="centerInside"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/text_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingHorizontal="@dimen/settingslib_expressive_space_small1"
+ android:paddingVertical="@dimen/settingslib_expressive_space_small2"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hyphenationFrequency="normalFast"
+ android:lineBreakWordStyle="phrase"
+ android:textAppearance="@style/TextAppearance.CardTitle.SettingsLib"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hyphenationFrequency="normalFast"
+ android:lineBreakWordStyle="phrase"
+ android:textAppearance="@style/TextAppearance.CardSummary.SettingsLib"/>
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@android:id/closeButton"
+ android:layout_width="@dimen/settingslib_expressive_space_medium4"
+ android:layout_height="@dimen/settingslib_expressive_space_medium4"
+ android:padding="@dimen/settingslib_expressive_space_extrasmall4"
+ android:layout_gravity="center"
+ android:src="@drawable/settingslib_expressive_icon_close"
+ android:background="?android:attr/selectableItemBackground" />
+
+ </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
\ No newline at end of file
diff --git a/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml
new file mode 100644
index 0000000..4cbdea5
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/res/values/styles_expressive.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <style name="TextAppearance.CardTitle.SettingsLib"
+ parent="@style/TextAppearance.PreferenceTitle.SettingsLib">
+ <item name="android:textColor">@color/settingslib_materialColorOnPrimary</item>
+ <item name="android:textSize">20sp</item>
+ </style>
+
+ <style name="TextAppearance.CardSummary.SettingsLib"
+ parent="@style/TextAppearance.PreferenceSummary.SettingsLib">
+ <item name="android:textColor">@color/settingslib_materialColorOnSecondary</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt
new file mode 100644
index 0000000..eb14746
--- /dev/null
+++ b/packages/SettingsLib/CardPreference/src/com/android/settingslib/widget/CardPreference.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settingslib.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+import com.android.settingslib.widget.preference.card.R
+
+/**
+ * The CardPreference shows a card like suggestion in homepage, which also support dismiss.
+ */
+class CardPreference @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+ init {
+ layoutResource = R.layout.settingslib_expressive_preference_card
+ }
+ private var dismissible = false
+ set(value) {
+ if (field != value) {
+ field = value
+ notifyChanged()
+ }
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+ holder.isDividerAllowedBelow = false
+ holder.isDividerAllowedAbove = false
+
+ holder.findViewById(android.R.id.closeButton)?.let { dismissButton ->
+ dismissButton.visibility = if (dismissible) View.VISIBLE else View.GONE
+ dismissButton.setOnClickListener {
+ isVisible = false
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v35/settingslib_expressive_icon_back.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v35/settingslib_expressive_icon_back.xml
new file mode 100644
index 0000000..ccbe20e
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/drawable-v35/settingslib_expressive_icon_back.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:start="16dp"
+ android:end="16dp"
+ android:top="4dp"
+ android:bottom="4dp">
+ <shape>
+ <size android:width="32dp" android:height="40dp" />
+ <solid android:color="@color/settingslib_materialColorSurfaceContainerHighest" />
+ <corners
+ android:radius="100dp" />
+ </shape>
+ </item>
+
+ <item
+ android:width="24dp"
+ android:height="24dp"
+ android:gravity="center"
+ android:start="16dp"
+ android:end="16dp"
+ android:top="4dp"
+ android:bottom="4dp">
+ <vector
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="@color/settingslib_materialColorOnSurfaceVariant"
+ android:autoMirrored="true">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M313,520L537,744L480,800L160,480L480,160L537,216L313,440L800,440L800,520L313,520Z"/>
+ </vector>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_base_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_base_layout.xml
new file mode 100644
index 0000000..b881c57
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_base_layout.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content_parent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <include layout="@layout/settingslib_expressive_collapsing_toolbar_content_layout"/>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_content_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_content_layout.xml
new file mode 100644
index 0000000..6221659
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v35/settingslib_expressive_collapsing_toolbar_content_layout.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/app_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true"
+ android:outlineAmbientShadowColor="@android:color/transparent"
+ android:outlineSpotShadowColor="@android:color/transparent"
+ android:background="@android:color/transparent"
+ app:expanded="false"
+ android:theme="@style/SettingsLibTheme.CollapsingToolbar.Expressive">
+
+ <com.google.android.material.appbar.CollapsingToolbarLayout
+ android:id="@+id/collapsing_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/settingslib_toolbar_layout_height"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
+ app:toolbarId="@id/action_bar"
+ style="@style/SettingsLibCollapsingToolbarLayoutStyle.Expressive">
+
+ <Toolbar
+ android:id="@+id/action_bar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:theme="?android:attr/actionBarTheme"
+ android:transitionName="shared_element_view"
+ app:layout_collapseMode="pin"/>
+
+ </com.google.android.material.appbar.CollapsingToolbarLayout>
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <FrameLayout
+ android:id="@+id/content_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
+</merge>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/styles_expressive.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/styles_expressive.xml
new file mode 100644
index 0000000..d58c2c2
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/styles_expressive.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <style name="SettingsLibCollapsingToolbarLayoutStyle">
+ <item name="expandedTitleTextAppearance">@style/SettingsLibCollapsingToolbarTitle.Expanded</item>
+ <item name="collapsedTitleTextAppearance">@style/SettingsLibCollapsingToolbarTitle.Collapsed</item>
+ <item name="expandedTitleMarginStart">@dimen/settingslib_expressive_space_small4</item>
+ <item name="expandedTitleMarginEnd">@dimen/settingslib_expressive_space_small4</item>
+ <item name="expandedTitleMarginBottom">@dimen/settingslib_expressive_space_medium1</item>
+ <item name="maxLines">3</item>
+ <item name="scrimVisibleHeightTrigger">@dimen/settingslib_scrim_visible_height_trigger</item>
+ <item name="contentScrim">@color/settingslib_materialColorSurfaceVariant</item>
+ <item name="statusBarScrim">@null</item>
+ <item name="scrimAnimationDuration">50</item>
+ <item name="collapsedTitleTextColor">@color/settingslib_materialColorOnSurface</item>
+ <item name="expandedTitleTextColor">@color/settingslib_materialColorOnSurface</item>
+ </style>
+ <style name="SettingsLibCollapsingToolbarLayoutStyle.Expressive">
+ <item name="contentScrim">@color/settingslib_materialColorSurfaceContainer</item>
+ </style>
+
+ <style name="SettingsLibCollapsingToolbarTitle.Collapsed" parent="@android:style/TextAppearance.DeviceDefault.Headline">
+ <!--set dp because we don't want size adjust when font size change-->
+ <item name="android:textSize">20dp</item>
+ </style>
+
+ <style name="SettingsLibCollapsingToolbarTitle.Expanded" parent="CollapsingToolbarTitle.Collapsed">
+ <item name="android:textSize">36dp</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/themes_expressive.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/themes_expressive.xml
new file mode 100644
index 0000000..ca1904a
--- /dev/null
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v35/themes_expressive.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <style name="SettingsLibTheme.CollapsingToolbar.Expressive" parent="@style/Theme.MaterialComponents.DayNight"/>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
index f46f110..feacecb 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java
@@ -27,6 +27,8 @@
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
+import com.android.settingslib.widget.SettingsThemeHelper;
+
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.color.DynamicColors;
@@ -69,7 +71,10 @@
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
DynamicColors.applyToActivityIfAvailable(this);
}
- setTheme(com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase);
+ int resId = SettingsThemeHelper.isExpressiveTheme(this)
+ ? com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase_Expressive
+ : com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase;
+ setTheme(resId);
if (mCustomizeLayoutResId > 0 && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
super.setContentView(mCustomizeLayoutResId);
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
index 16ed5a8..0f9d94e 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
@@ -28,6 +28,8 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
+import com.android.settingslib.widget.SettingsThemeHelper;
+
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
@@ -59,6 +61,11 @@
protected void onCreate(@Nullable Bundle savedInstanceState) {
EdgeToEdgeUtils.enable(this);
super.onCreate(savedInstanceState);
+
+ if (SettingsThemeHelper.isExpressiveTheme(this)) {
+ setTheme(com.android.settingslib.widget.theme.R.style.Theme_SubSettingsBase_Expressive);
+ }
+
// for backward compatibility on R devices or wearable devices due to small device size.
if (mCustomizeLayoutResId > 0 && (Build.VERSION.SDK_INT < Build.VERSION_CODES.S
|| isWatch())) {
@@ -66,7 +73,7 @@
return;
}
- View view = getToolbarDelegate().onCreateView(getLayoutInflater(), null);
+ View view = getToolbarDelegate().onCreateView(getLayoutInflater(), null, this);
super.setContentView(view);
}
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
index 2ab2abd..01ecb66 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
@@ -20,6 +20,7 @@
import android.app.ActionBar;
import android.app.Activity;
+import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
@@ -37,6 +38,8 @@
import androidx.appcompat.app.AppCompatActivity;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import com.android.settingslib.widget.SettingsThemeHelper;
+
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
@@ -84,6 +87,8 @@
private boolean mUseCollapsingToolbar;
+ private boolean mIsExpressiveTheme;
+
public CollapsingToolbarDelegate(@NonNull HostCallback hostCallback,
boolean useCollapsingToolbar) {
mHostCallback = hostCallback;
@@ -103,11 +108,16 @@
int layoutId;
boolean useCollapsingToolbar =
mUseCollapsingToolbar || Build.VERSION.SDK_INT < Build.VERSION_CODES.S;
+ Context context = (activity != null) ? activity : inflater.getContext();
+ mIsExpressiveTheme = SettingsThemeHelper.isExpressiveTheme(context);
if (useCollapsingToolbar) {
- layoutId = R.layout.collapsing_toolbar_base_layout;
+ layoutId = mIsExpressiveTheme
+ ? R.layout.settingslib_expressive_collapsing_toolbar_base_layout
+ : R.layout.collapsing_toolbar_base_layout;
} else {
layoutId = R.layout.non_collapsing_toolbar_base_layout;
}
+
final View view = inflater.inflate(layoutId, container, false);
if (view instanceof CoordinatorLayout) {
mCoordinatorLayout = (CoordinatorLayout) view;
@@ -155,6 +165,9 @@
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
+ if (mIsExpressiveTheme) {
+ actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+ }
actionBar.setDisplayShowTitleEnabled(true);
}
}
@@ -174,6 +187,9 @@
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
+ if (mIsExpressiveTheme) {
+ actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+ }
actionBar.setDisplayShowTitleEnabled(true);
}
}
@@ -188,6 +204,9 @@
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
+ if (mIsExpressiveTheme) {
+ actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+ }
actionBar.setDisplayShowTitleEnabled(true);
}
}
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
index f70add9..51d7504 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
@@ -39,6 +39,7 @@
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.android.settingslib.collapsingtoolbar.R;
+import com.android.settingslib.widget.SettingsThemeHelper;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
@@ -105,7 +106,10 @@
}
private void init() {
- inflate(getContext(), R.layout.collapsing_toolbar_content_layout, this);
+ int resId = SettingsThemeHelper.isExpressiveTheme(getContext())
+ ? R.layout.settingslib_expressive_collapsing_toolbar_content_layout
+ : R.layout.collapsing_toolbar_content_layout;
+ inflate(getContext(), resId, this);
mCollapsingToolbarLayout = findViewById(R.id.collapsing_toolbar);
mAppBarLayout = findViewById(R.id.app_bar);
if (mCollapsingToolbarLayout != null) {
@@ -172,6 +176,9 @@
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayShowTitleEnabled(true);
+ if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+ }
}
}
@@ -202,6 +209,9 @@
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayShowTitleEnabled(true);
+ if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ actionBar.setHomeAsUpIndicator(R.drawable.settingslib_expressive_icon_back);
+ }
}
}
diff --git a/packages/SettingsLib/MainSwitchPreference/res/drawable-v35/settingslib_expressive_switch_bar_bg.xml b/packages/SettingsLib/MainSwitchPreference/res/drawable-v35/settingslib_expressive_switch_bar_bg.xml
new file mode 100644
index 0000000..3999e76
--- /dev/null
+++ b/packages/SettingsLib/MainSwitchPreference/res/drawable-v35/settingslib_expressive_switch_bar_bg.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/settingslib_materialColorPrimaryContainer"/>
+ <corners android:radius="@dimen/settingslib_expressive_radius_extralarge3"/>
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_bar.xml
new file mode 100644
index 0000000..4425ef0
--- /dev/null
+++ b/packages/SettingsLib/MainSwitchPreference/res/layout-v35/settingslib_expressive_main_switch_bar.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingVertical="@dimen/settingslib_expressive_space_small1"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/frame"
+ android:minHeight="@dimen/settingslib_expressive_space_large3"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:paddingStart="@dimen/settingslib_expressive_space_medium1"
+ android:paddingEnd="@dimen/settingslib_expressive_space_small3"
+ android:background="@drawable/settingslib_expressive_switch_bar_bg">
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginEnd="@dimen/settingslib_expressive_space_extrasmall4"
+ android:layout_marginVertical="@dimen/settingslib_expressive_space_small4"
+ android:layout_weight="1">
+
+ <TextView
+ android:id="@+id/switch_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:ellipsize="end"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:textColor="@color/settingslib_main_switch_text_color" />
+
+ <TextView
+ android:id="@+id/switch_summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignLeft="@id/switch_text"
+ android:layout_alignStart="@id/switch_text"
+ android:layout_below="@id/switch_text"
+ android:layout_gravity="start"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ android:textColor="@color/settingslib_main_switch_text_color"
+ android:visibility="gone" />
+ </RelativeLayout>
+
+ <com.google.android.material.materialswitch.MaterialSwitch
+ android:theme="@style/Theme.Material3.DynamicColors.DayNight"
+ android:id="@android:id/switch_widget"
+ style="@style/SettingslibMainSwitchStyle.Expressive" />
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
index e6f61a8..106802e 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
@@ -22,6 +22,7 @@
import android.os.Build.VERSION_CODES;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -31,6 +32,7 @@
import android.widget.TextView;
import androidx.annotation.ColorInt;
+import androidx.annotation.RequiresApi;
import com.android.settingslib.widget.mainswitch.R;
@@ -52,6 +54,7 @@
private int mBackgroundActivatedColor;
protected TextView mTextView;
+ protected TextView mSummaryView;
protected CompoundButton mSwitch;
private final View mFrameView;
@@ -71,7 +74,11 @@
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- LayoutInflater.from(context).inflate(R.layout.settingslib_main_switch_bar, this);
+ boolean isExpressive = SettingsThemeHelper.isExpressiveTheme(context);
+ int resId = isExpressive
+ ? R.layout.settingslib_expressive_main_switch_bar
+ : R.layout.settingslib_main_switch_bar;
+ LayoutInflater.from(context).inflate(resId, this);
if (Build.VERSION.SDK_INT < VERSION_CODES.S) {
TypedArray a;
@@ -93,6 +100,9 @@
mFrameView = findViewById(R.id.frame);
mTextView = findViewById(R.id.switch_text);
+ if (isExpressive) {
+ mSummaryView = findViewById(R.id.switch_summary);
+ }
mSwitch = findViewById(android.R.id.switch_widget);
addOnSwitchChangeListener((switchView, isChecked) -> setChecked(isChecked));
@@ -109,6 +119,12 @@
final CharSequence title = a.getText(
androidx.preference.R.styleable.Preference_android_title);
setTitle(title);
+ //TODO(b/369470034): update to next version
+ if (isExpressive && Build.VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM) {
+ CharSequence summary = a.getText(
+ androidx.preference.R.styleable.Preference_android_summary);
+ setSummary(summary);
+ }
a.recycle();
}
@@ -153,6 +169,18 @@
}
/**
+ * Set the summary text
+ */
+ @RequiresApi(VERSION_CODES.VANILLA_ICE_CREAM)
+ //TODO(b/369470034): update to next version
+ public void setSummary(CharSequence text) {
+ if (mSummaryView != null) {
+ mSummaryView.setText(text);
+ mSummaryView.setVisibility(TextUtils.isEmpty(text) ? GONE : VISIBLE);
+ }
+ }
+
+ /**
* Set icon space reserved for title
*/
public void setIconSpaceReserved(boolean iconSpaceReserved) {
@@ -219,6 +247,12 @@
mFrameView.setEnabled(enabled);
mFrameView.setActivated(isChecked());
}
+
+ if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ if (mSummaryView != null) {
+ mSummaryView.setEnabled(enabled);
+ }
+ }
}
private void propagateChecked(boolean isChecked) {
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
index b294d4e..d895c87 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.content.res.TypedArray;
+import android.os.Build;
import android.util.AttributeSet;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
@@ -89,6 +90,10 @@
androidx.preference.R.styleable.Preference_android_title);
setTitle(title);
+ CharSequence summary = a.getText(
+ androidx.preference.R.styleable.Preference_android_summary);
+ setSummary(summary);
+
final boolean bIconSpaceReserved = a.getBoolean(
androidx.preference.R.styleable.Preference_android_iconSpaceReserved, true);
setIconSpaceReserved(bIconSpaceReserved);
@@ -113,6 +118,15 @@
}
@Override
+ public void setSummary(CharSequence summary) {
+ super.setSummary(summary);
+ if (mMainSwitchBar != null
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ mMainSwitchBar.setSummary(summary);
+ }
+ }
+
+ @Override
public void setIconSpaceReserved(boolean iconSpaceReserved) {
super.setIconSpaceReserved(iconSpaceReserved);
if (mMainSwitchBar != null) {
diff --git a/packages/SettingsLib/Service/Android.bp b/packages/SettingsLib/Service/Android.bp
new file mode 100644
index 0000000..65d0e4c
--- /dev/null
+++ b/packages/SettingsLib/Service/Android.bp
@@ -0,0 +1,20 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+ name: "SettingsLibService-srcs",
+ srcs: ["src/**/*.kt"],
+}
+
+android_library {
+ name: "SettingsLibService",
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+ srcs: [":SettingsLibService-srcs"],
+ static_libs: [
+ "SettingsLibGraph",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Service/AndroidManifest.xml b/packages/SettingsLib/Service/AndroidManifest.xml
new file mode 100644
index 0000000..56d7818
--- /dev/null
+++ b/packages/SettingsLib/Service/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.service">
+
+ <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.kt
new file mode 100644
index 0000000..95661c9
--- /dev/null
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceGraphApi.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.settingslib.service
+
+import android.app.Application
+import com.android.settingslib.graph.GetPreferenceGraphApiHandler
+import com.android.settingslib.graph.GetPreferenceGraphRequest
+
+/** Api to get preference graph. */
+internal class PreferenceGraphApi(activityClasses: Set<String>) :
+ GetPreferenceGraphApiHandler(activityClasses) {
+ override val id: Int
+ get() = API_GET_PREFERENCE_GRAPH
+
+ override fun hasPermission(
+ application: Application,
+ myUid: Int,
+ callingUid: Int,
+ request: GetPreferenceGraphRequest,
+ ): Boolean {
+ return true // TODO: add permission check
+ }
+}
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.kt
new file mode 100644
index 0000000..d382dad
--- /dev/null
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/PreferenceService.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.settingslib.service
+
+import com.android.settingslib.ipc.ApiHandler
+import com.android.settingslib.ipc.MessengerService
+import com.android.settingslib.ipc.PermissionChecker
+
+/**
+ * Preference service providing a bunch of APIs.
+ *
+ * In AndroidManifest.xml, the <service> must specify <intent-filter> with action
+ * [PREFERENCE_SERVICE_ACTION].
+ */
+open class PreferenceService(
+ permissionChecker: PermissionChecker,
+ name: String = "PreferenceService",
+) :
+ MessengerService(
+ listOf<ApiHandler<*, *>>(PreferenceGraphApi(setOf())),
+ permissionChecker,
+ name,
+ )
diff --git a/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
new file mode 100644
index 0000000..8f03111
--- /dev/null
+++ b/packages/SettingsLib/Service/src/com/android/settingslib/service/ServiceApiConstants.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.service
+
+const val PREFERENCE_SERVICE_ACTION = "com.android.settingslib.PREFERENCE_SERVICE"
+
+internal const val API_GET_PREFERENCE_GRAPH = 1
diff --git a/packages/SettingsLib/SettingsTheme/res/color-v35/settingslib_expressive_color_main_switch_track.xml b/packages/SettingsLib/SettingsTheme/res/color-v35/settingslib_expressive_color_main_switch_track.xml
new file mode 100644
index 0000000..a6885a4
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/color-v35/settingslib_expressive_color_main_switch_track.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- State Disabled, Unchecked -->
+ <item
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+ android:state_enabled="false" android:state_checked="false"/>
+ <!-- State Disabled, Checked -->
+ <item
+ android:alpha="?android:attr/disabledAlpha"
+ android:color="@color/settingslib_materialColorPrimary"
+ android:state_enabled="false" android:state_checked="true"/>
+ <!-- State Checked -->
+ <item
+ android:color="@color/settingslib_materialColorPrimary"
+ android:state_checked="true"/>
+
+ <!-- State Unchecked -->
+ <item
+ android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+ android:state_hovered="true" android:state_checked="false"/>
+ <item
+ android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+ android:state_focused="true" android:state_checked="false"/>
+ <item
+ android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+ android:state_pressed="true" android:state_checked="false"/>
+ <item
+ android:color="@color/settingslib_materialColorSurfaceContainerHighest"
+ android:state_checked="false"/>
+</selector>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml
new file mode 100644
index 0000000..9216c96
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/drawable/settingslib_expressive_icon_bullet_start.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?android:attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M354,673L480,597L606,674L573,530L684,434L538,421L480,285L422,420L276,433L387,530L354,673ZM233,840L298,559L80,370L368,345L480,80L592,345L880,370L662,559L727,840L480,691L233,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml
index dda7517c..952562e 100644
--- a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference.xml
@@ -31,37 +31,7 @@
<include layout="@layout/settingslib_icon_frame"/>
- <RelativeLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:paddingTop="16dp"
- android:paddingBottom="16dp">
-
- <TextView
- android:id="@android:id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="start"
- android:textAlignment="viewStart"
- android:maxLines="2"
- android:textAppearance="?android:attr/textAppearanceListItem"
- android:ellipsize="marquee"/>
-
- <TextView
- android:id="@android:id/summary"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@android:id/title"
- android:layout_alignLeft="@android:id/title"
- android:layout_alignStart="@android:id/title"
- android:layout_gravity="start"
- android:textAlignment="viewStart"
- android:textColor="?android:attr/textColorSecondary"
- android:maxLines="10"
- style="@style/PreferenceSummaryTextStyle"/>
-
- </RelativeLayout>
+ <include layout="@layout/settingslib_preference_frame"/>
<!-- Preference should place its actual preference widget here. -->
<LinearLayout
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml
new file mode 100644
index 0000000..433d264
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v31/settingslib_preference_frame.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:textAlignment="viewStart"
+ android:text="Title"
+ android:maxLines="2"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:ellipsize="marquee"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/title"
+ android:layout_alignLeft="@android:id/title"
+ android:layout_alignStart="@android:id/title"
+ android:text="Summary summary summary"
+ android:layout_gravity="start"
+ android:textAlignment="viewStart"
+ android:textColor="?android:attr/textColorSecondary"
+ android:maxLines="10"
+ style="@style/PreferenceSummaryTextStyle"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
index fedcc77..4e23b65 100644
--- a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference.xml
@@ -31,41 +31,7 @@
<include layout="@layout/settingslib_icon_frame"/>
- <RelativeLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:paddingTop="16dp"
- android:paddingBottom="16dp">
-
- <TextView
- android:id="@android:id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="start"
- android:textAlignment="viewStart"
- android:maxLines="2"
- android:hyphenationFrequency="normalFast"
- android:lineBreakWordStyle="phrase"
- android:textAppearance="?android:attr/textAppearanceListItem"
- android:ellipsize="marquee"/>
-
- <TextView
- android:id="@android:id/summary"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@android:id/title"
- android:layout_alignLeft="@android:id/title"
- android:layout_alignStart="@android:id/title"
- android:layout_gravity="start"
- android:textAlignment="viewStart"
- android:textColor="?android:attr/textColorSecondary"
- android:maxLines="10"
- android:hyphenationFrequency="normalFast"
- android:lineBreakWordStyle="phrase"
- style="@style/PreferenceSummaryTextStyle"/>
-
- </RelativeLayout>
+ <include layout="@layout/settingslib_preference_frame"/>
<!-- Preference should place its actual preference widget here. -->
<LinearLayout
diff --git a/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml
new file mode 100644
index 0000000..f93e1b9
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/layout-v33/settingslib_preference_frame.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:textAlignment="viewStart"
+ android:text="Title"
+ android:maxLines="2"
+ android:hyphenationFrequency="normalFast"
+ android:lineBreakWordStyle="phrase"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:ellipsize="marquee"/>
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/title"
+ android:layout_alignLeft="@android:id/title"
+ android:layout_alignStart="@android:id/title"
+ android:layout_gravity="start"
+ android:textAlignment="viewStart"
+ android:textColor="?android:attr/textColorSecondary"
+ android:maxLines="10"
+ android:text="Summary summary summary"
+ android:hyphenationFrequency="normalFast"
+ android:lineBreakWordStyle="phrase"
+ style="@style/PreferenceSummaryTextStyle"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml
index 2320aab..0542c51 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/dimens_expressive.xml
@@ -48,6 +48,7 @@
<dimen name="settingslib_expressive_space_medium2">36dp</dimen>
<dimen name="settingslib_expressive_space_medium3">40dp</dimen>
<dimen name="settingslib_expressive_space_medium4">48dp</dimen>
+ <dimen name="settingslib_expressive_space_medium5">56dp</dimen>
<dimen name="settingslib_expressive_space_large1">60dp</dimen>
<dimen name="settingslib_expressive_space_large2">64dp</dimen>
<dimen name="settingslib_expressive_space_large3">72dp</dimen>
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
index 04ae80e..dc2eb64 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
@@ -169,4 +169,39 @@
<item name="android:focusable">false</item>
<item name="thumbIcon">@drawable/settingslib_expressive_switch_thumb_icon</item>
</style>
+
+ <style name="SettingsLibButtonStyle.Expressive.Tonal"
+ parent="@style/Widget.Material3.Button.TonalButton">
+ <item name="android:theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:gravity">center</item>
+ <item name="android:minWidth">@dimen/settingslib_expressive_space_medium4</item>
+ <item name="android:minHeight">@dimen/settingslib_expressive_space_medium4</item>
+ <item name="android:paddingVertical">@dimen/settingslib_expressive_space_extrasmall5</item>
+ <item name="android:paddingHorizontal">@dimen/settingslib_expressive_space_small1</item>
+ <item name="android:backgroundTint">@color/settingslib_materialColorSecondaryContainer</item>
+ <item name="android:textAppearance">@android:style/TextAppearance.DeviceDefault.Medium</item>
+ <item name="android:textColor">@color/settingslib_materialColorOnSecondaryContainer</item>
+ <item name="android:textSize">14sp</item>
+ <item name="iconGravity">textStart</item>
+ <item name="iconTint">@color/settingslib_materialColorOnSecondaryContainer</item>
+ <item name="iconSize">@dimen/settingslib_expressive_space_small4</item>
+ </style>
+
+ <style name="SettingslibMainSwitchStyle.Expressive" parent="SettingslibSwitchStyle.Expressive">
+ <item name="android:layout_gravity">center</item>
+ <item name="trackTint">@color/settingslib_expressive_color_main_switch_track</item>
+ </style>
+
+ <style name="SettingsLibCardStyle" parent="">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginHorizontal">?android:attr/listPreferredItemPaddingStart</item>
+ <item name="android:layout_marginVertical">@dimen/settingslib_expressive_space_extrasmall4</item>
+ <item name="cardBackgroundColor">@color/settingslib_materialColorPrimary</item>
+ <item name="cardCornerRadius">@dimen/settingslib_expressive_radius_extralarge3</item>
+ <item name="cardElevation">0dp</item>
+ <item name="rippleColor">?android:attr/colorControlHighlight</item>
+ </style>
</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index d01c0b9..272dc2d 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,7 +15,7 @@
#
[versions]
-agp = "8.6.0"
+agp = "8.6.1"
compose-compiler = "1.5.11"
dexmaker-mockito = "2.28.3"
jvm = "17"
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip
deleted file mode 100644
index 50432f3..0000000
--- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip
+++ /dev/null
Binary files differ
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10.2-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10.2-bin.zip
new file mode 100644
index 0000000..45f0424
--- /dev/null
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10.2-bin.zip
Binary files differ
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
index 9a7f4b6..1c25e974 100644
--- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
@@ -16,6 +16,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=gradle-8.10-bin.zip
+distributionUrl=gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts
index 790aa9f..8f8275b 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle.kts
+++ b/packages/SettingsLib/Spa/spa/build.gradle.kts
@@ -58,12 +58,12 @@
api("androidx.compose.material:material-icons-extended:$jetpackComposeVersion")
api("androidx.compose.runtime:runtime-livedata:$jetpackComposeVersion")
api("androidx.compose.ui:ui-tooling-preview:$jetpackComposeVersion")
+ api("androidx.graphics:graphics-shapes-android:1.0.1")
api("androidx.lifecycle:lifecycle-livedata-ktx")
api("androidx.lifecycle:lifecycle-runtime-compose")
api("androidx.navigation:navigation-compose:2.8.1")
api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha")
- api("com.google.android.material:material:1.11.0")
- api("androidx.graphics:graphics-shapes-android:1.0.1")
+ api("com.google.android.material:material:1.12.0")
debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion")
implementation("com.airbnb.android:lottie-compose:6.4.0")
@@ -84,7 +84,6 @@
// Excludes files forked from AndroidX.
"com/android/settingslib/spa/widget/scaffold/CustomizedAppBar*",
- "com/android/settingslib/spa/widget/scaffold/TopAppBarColors*",
// Excludes files forked from Accompanist.
"com/android/settingslib/spa/framework/compose/DrawablePainter*",
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
index 460bf99..965c971 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTypography.kt
@@ -162,3 +162,6 @@
/** Creates a new [TextStyle] which font weight set to medium. */
internal fun TextStyle.toMediumWeight() =
copy(fontWeight = FontWeight.Medium, letterSpacing = 0.01.em)
+
+internal fun TextStyle.toSemiBoldWeight() =
+ copy(fontWeight = FontWeight.SemiBold, letterSpacing = 0.01.em)
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
index 7e1df16..5bb57b8 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt
@@ -110,7 +110,7 @@
shape = RectangleShape,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
- contentColor = MaterialTheme.colorScheme.onPrimary,
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
disabledContainerColor = MaterialTheme.colorScheme.surface,
),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
index 94d2c21..f99d206 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/Actions.kt
@@ -41,9 +41,7 @@
internal fun NavigateBack() {
val navController = LocalNavController.current
val contentDescription = stringResource(R.string.abc_action_bar_up_description)
- BackAction(contentDescription) {
- navController.navigateBack()
- }
+ BackAction(contentDescription) { navController.navigateBack() }
}
/** Action that collapses the search bar. */
@@ -55,15 +53,35 @@
@Composable
private fun BackAction(contentDescription: String, onClick: () -> Unit) {
- IconButton(onClick) {
+ IconButton(
+ onClick = onClick,
+ modifier =
+ if (isSpaExpressiveEnabled)
+ Modifier
+ .padding(
+ start = SettingsDimension.paddingLarge,
+ end = SettingsDimension.paddingSmall,
+ top = SettingsDimension.paddingExtraSmall,
+ bottom = SettingsDimension.paddingExtraSmall,
+ )
+ .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight)
+ .clip(SettingsShape.CornerExtraLarge)
+ else Modifier,
+ ) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = contentDescription,
- modifier = if (isSpaExpressiveEnabled) Modifier
- .size(SettingsDimension.actionIconWidth, SettingsDimension.actionIconHeight)
- .clip(SettingsShape.CornerExtraLarge)
- .background(MaterialTheme.colorScheme.surfaceContainerHigh)
- .padding(SettingsDimension.actionIconPadding) else Modifier
+ modifier =
+ if (isSpaExpressiveEnabled)
+ Modifier
+ .size(
+ SettingsDimension.actionIconWidth,
+ SettingsDimension.actionIconHeight,
+ )
+ .clip(SettingsShape.CornerExtraLarge)
+ .background(MaterialTheme.colorScheme.surfaceContainerHighest)
+ .padding(SettingsDimension.actionIconPadding)
+ else Modifier,
)
}
}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
index 2ae3b56..2c55779 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt
@@ -78,7 +78,9 @@
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
import com.android.settingslib.spa.framework.theme.settingsBackground
+import com.android.settingslib.spa.framework.theme.toSemiBoldWeight
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
@@ -116,8 +118,12 @@
) {
TwoRowsTopAppBar(
title = { Title(title = title, maxLines = 3) },
- titleTextStyle = MaterialTheme.typography.displaySmall,
- smallTitleTextStyle = MaterialTheme.typography.titleMedium,
+ titleTextStyle =
+ if (isSpaExpressiveEnabled) MaterialTheme.typography.displaySmall.toSemiBoldWeight()
+ else MaterialTheme.typography.displaySmall,
+ smallTitleTextStyle =
+ if (isSpaExpressiveEnabled) MaterialTheme.typography.titleLarge.toSemiBoldWeight()
+ else MaterialTheme.typography.titleLarge,
titleBottomPadding = LargeTitleBottomPadding,
smallTitle = { Title(title = title, maxLines = 1) },
modifier = modifier,
@@ -136,7 +142,9 @@
text = title,
modifier =
Modifier.padding(
- start = SettingsDimension.itemPaddingAround,
+ start =
+ if (isSpaExpressiveEnabled) SettingsDimension.paddingExtraSmall
+ else SettingsDimension.itemPaddingAround,
end = SettingsDimension.itemPaddingEnd,
)
.semantics { heading() },
@@ -194,7 +202,7 @@
return lerp(
containerColor,
scrolledContainerColor,
- FastOutLinearInEasing.transform(colorTransitionFraction)
+ FastOutLinearInEasing.transform(colorTransitionFraction),
)
}
@@ -241,7 +249,7 @@
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
- content = actions
+ content = actions,
)
}
@@ -296,7 +304,7 @@
windowInsets: WindowInsets,
colors: TopAppBarColors,
pinnedHeight: Dp,
- scrollBehavior: TopAppBarScrollBehavior?
+ scrollBehavior: TopAppBarScrollBehavior?,
) {
if (MaxHeightWithoutTitle <= pinnedHeight) {
throw IllegalArgumentException(
@@ -333,7 +341,7 @@
Row(
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
- content = actions
+ content = actions,
)
}
val topTitleAlpha = { TopTitleAlphaEasing.transform(colorTransitionFraction()) }
@@ -356,9 +364,9 @@
scrollBehavior.state,
velocity,
scrollBehavior.flingAnimationSpec,
- scrollBehavior.snapAnimationSpec
+ scrollBehavior.snapAnimationSpec,
)
- }
+ },
)
} else {
Modifier
@@ -412,7 +420,8 @@
val measuredMaxHeightPx =
density.run {
MaxHeightWithoutTitle.toPx() +
- coordinates.size.height.toFloat()
+ coordinates.size.height.toFloat() +
+ titleBaselineHeight.toPx()
}
// Allow larger max height for multi-line title, but do not reduce
// max height to prevent flaky.
@@ -430,7 +439,7 @@
titleBottomPadding = titleBottomPaddingPx,
hideTitleSemantics = hideBottomRowSemantics,
navigationIcon = {},
- actions = {}
+ actions = {},
)
}
}
@@ -485,7 +494,7 @@
Box(Modifier.layoutId("navigationIcon").padding(start = TopAppBarHorizontalPadding)) {
CompositionLocalProvider(
LocalContentColor provides navigationIconContentColor,
- content = navigationIcon
+ content = navigationIcon,
)
}
Box(
@@ -504,18 +513,18 @@
fontScale = if (titleScaleDisabled) 1f else fontScale,
)
},
- content = title
+ content = title,
)
}
}
Box(Modifier.layoutId("actionIcons").padding(end = TopAppBarHorizontalPadding)) {
CompositionLocalProvider(
LocalContentColor provides actionIconContentColor,
- content = actions
+ content = actions,
)
}
},
- modifier = modifier
+ modifier = modifier,
) { measurables, constraints ->
val navigationIconPlaceable =
measurables
@@ -552,7 +561,7 @@
// Navigation icon
navigationIconPlaceable.placeRelative(
x = 0,
- y = (layoutHeight - navigationIconPlaceable.height) / 2
+ y = (layoutHeight - navigationIconPlaceable.height) / 2,
)
// Title
@@ -570,17 +579,17 @@
titlePlaceable.height -
max(
0,
- titleBottomPadding - titlePlaceable.height + titleBaseline
+ titleBottomPadding - titlePlaceable.height + titleBaseline,
)
// Arrangement.Top
else -> 0
- }
+ },
)
// Action icons
actionIconsPlaceable.placeRelative(
x = constraints.maxWidth - actionIconsPlaceable.width,
- y = (layoutHeight - actionIconsPlaceable.height) / 2
+ y = (layoutHeight - actionIconsPlaceable.height) / 2,
)
}
}
@@ -595,7 +604,7 @@
state: TopAppBarState,
velocity: Float,
flingAnimationSpec: DecayAnimationSpec<Float>?,
- snapAnimationSpec: AnimationSpec<Float>?
+ snapAnimationSpec: AnimationSpec<Float>?,
): Velocity {
// Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
// and just return Zero Velocity.
@@ -609,20 +618,18 @@
// continue the motion to expand or collapse the app bar.
if (flingAnimationSpec != null && abs(velocity) > 1f) {
var lastValue = 0f
- AnimationState(
- initialValue = 0f,
- initialVelocity = velocity,
- )
- .animateDecay(flingAnimationSpec) {
- val delta = value - lastValue
- val initialHeightOffset = state.heightOffset
- state.heightOffset = initialHeightOffset + delta
- val consumed = abs(initialHeightOffset - state.heightOffset)
- lastValue = value
- remainingVelocity = this.velocity
- // avoid rounding errors and stop if anything is unconsumed
- if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
- }
+ AnimationState(initialValue = 0f, initialVelocity = velocity).animateDecay(
+ flingAnimationSpec
+ ) {
+ val delta = value - lastValue
+ val initialHeightOffset = state.heightOffset
+ state.heightOffset = initialHeightOffset + delta
+ val consumed = abs(initialHeightOffset - state.heightOffset)
+ lastValue = value
+ remainingVelocity = this.velocity
+ // avoid rounding errors and stop if anything is unconsumed
+ if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
+ }
}
// Snap if animation specs were provided.
if (snapAnimationSpec != null) {
@@ -633,7 +640,7 @@
} else {
state.heightOffsetLimit
},
- animationSpec = snapAnimationSpec
+ animationSpec = snapAnimationSpec,
) {
state.heightOffset = value
}
@@ -647,9 +654,10 @@
// Medium or Large app bar.
private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f)
-internal val MaxHeightWithoutTitle = 124.dp
+internal val MaxHeightWithoutTitle = if (isSpaExpressiveEnabled) 84.dp else 124.dp
internal val DefaultTitleHeight = 52.dp
internal val ContainerHeight = 56.dp
+private val titleBaselineHeight = if (isSpaExpressiveEnabled) 8.dp else 0.dp
private val LargeTitleBottomPadding = 28.dp
private val TopAppBarHorizontalPadding = 4.dp
diff --git a/packages/SettingsLib/ZeroStatePreference/Android.bp b/packages/SettingsLib/ZeroStatePreference/Android.bp
new file mode 100644
index 0000000..4fc00bd
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/Android.bp
@@ -0,0 +1,33 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+ name: "SettingsLibZeroStatePreference",
+ use_resource_processor: true,
+ defaults: [
+ "SettingsLintDefaults",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ resource_dirs: ["res"],
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.preference_preference",
+ "SettingsLibSettingsTheme",
+ ],
+ sdk_version: "system_current",
+ min_sdk_version: "28",
+ apex_available: [
+ "//apex_available:platform",
+ ],
+}
diff --git a/packages/SettingsLib/ZeroStatePreference/AndroidManifest.xml b/packages/SettingsLib/ZeroStatePreference/AndroidManifest.xml
new file mode 100644
index 0000000..51b0ab8
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.widget.preference.zerostate">
+
+ <uses-sdk android:minSdkVersion="28" />
+
+</manifest>
diff --git a/packages/SettingsLib/ZeroStatePreference/res/drawable/settingslib_expressive_zerostate_background.xml b/packages/SettingsLib/ZeroStatePreference/res/drawable/settingslib_expressive_zerostate_background.xml
new file mode 100644
index 0000000..f42b441
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/res/drawable/settingslib_expressive_zerostate_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="160dp"
+ android:height="160dp"
+ android:viewportWidth="161"
+ android:viewportHeight="161">
+ <path
+ android:pathData="M67.2,4.43C74.79,-1.41 85.35,-1.41 92.94,4.43L112.01,19.1C113.48,20.23 115.09,21.16 116.8,21.87L139.02,31.08C147.86,34.74 153.14,43.9 151.89,53.4L148.74,77.28C148.5,79.12 148.5,80.98 148.74,82.82L151.89,106.71C153.14,116.2 147.86,125.36 139.02,129.03L116.8,138.23C115.09,138.95 113.48,139.87 112.01,141L92.94,155.67C85.35,161.51 74.79,161.51 67.2,155.67L48.13,141C46.66,139.87 45.05,138.95 43.34,138.23L21.12,129.03C12.28,125.36 7,116.2 8.25,106.71L11.4,82.82C11.64,80.98 11.64,79.12 11.4,77.28L8.25,53.4C7,43.9 12.28,34.74 21.12,31.08L43.34,21.87C45.05,21.16 46.66,20.23 48.13,19.1L67.2,4.43Z"
+ android:fillColor="@color/settingslib_materialColorSurfaceContainerHigh"/>
+</vector>
diff --git a/packages/SettingsLib/ZeroStatePreference/res/layout/settingslib_expressive_preference_zerostate.xml b/packages/SettingsLib/ZeroStatePreference/res/layout/settingslib_expressive_preference_zerostate.xml
new file mode 100644
index 0000000..c0b195c
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/res/layout/settingslib_expressive_preference_zerostate.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ tools:ignore="ContentDescription">
+
+ <ImageView
+ android:layout_width="@dimen/settingslib_expressive_zero_state_background_size"
+ android:layout_height="@dimen/settingslib_expressive_zero_state_background_size"
+ android:src="@drawable/settingslib_expressive_zerostate_background"/>
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/settingslib_expressive_space_large3"
+ android:layout_height="@dimen/settingslib_expressive_space_large3"
+ android:layout_gravity="center"/>
+
+ </FrameLayout>
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="@dimen/settingslib_expressive_zero_state_title_width"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_small4"/>
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="@dimen/settingslib_expressive_zero_state_title_width"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_small4"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/ZeroStatePreference/res/values/dimens.xml b/packages/SettingsLib/ZeroStatePreference/res/values/dimens.xml
new file mode 100644
index 0000000..e981ecc
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <dimen name="settingslib_expressive_zero_state_background_size">160dp</dimen>
+ <dimen name="settingslib_expressive_zero_state_title_width">316dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/ZeroStatePreference/src/com/android/settingslib/widget/ZeroStatePreference.kt b/packages/SettingsLib/ZeroStatePreference/src/com/android/settingslib/widget/ZeroStatePreference.kt
new file mode 100644
index 0000000..9b1ccef
--- /dev/null
+++ b/packages/SettingsLib/ZeroStatePreference/src/com/android/settingslib/widget/ZeroStatePreference.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.widget
+
+import android.content.Context
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.widget.ImageView
+import androidx.preference.Preference
+import androidx.preference.PreferenceViewHolder
+
+import com.android.settingslib.widget.preference.zerostate.R
+
+class ZeroStatePreference @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+ defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes) {
+
+ private val iconTint: Int = context.getColor(
+ com.android.settingslib.widget.theme.R.color.settingslib_materialColorOnSecondaryContainer
+ )
+ private var tintedIcon: Drawable? = null
+
+ init {
+ isSelectable = false
+ layoutResource = R.layout.settingslib_expressive_preference_zerostate
+ icon?.let { originalIcon ->
+ tintedIcon = originalIcon.mutate().apply {
+ colorFilter = PorterDuffColorFilter(iconTint, PorterDuff.Mode.SRC_IN)
+ }
+ }
+ }
+
+ override fun onBindViewHolder(holder: PreferenceViewHolder) {
+ super.onBindViewHolder(holder)
+
+ holder.itemView.isFocusable = false
+ holder.itemView.isClickable = false
+
+ (holder.findViewById(android.R.id.icon) as? ImageView)?.apply {
+ setImageDrawable(tintedIcon ?: icon)
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
index 3d8ff86..d4f2336 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingServiceConnection.kt
@@ -160,6 +160,12 @@
}
.shareIn(scope = coroutineScope, started = SharingStarted.WhileSubscribed(), replay = 1)
+ private val services =
+ ConcurrentHashMap<
+ EndPoint,
+ StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>,
+ >()
+
/** Gets [DeviceSettingsConfig] for the device, return null when failed. */
suspend fun getDeviceSettingsConfig(): DeviceSettingsConfig? {
if (!isServiceEnabled.await()) {
@@ -320,11 +326,5 @@
const val CONFIG_SERVICE_PACKAGE_NAME = "DEVICE_SETTINGS_CONFIG_PACKAGE_NAME"
const val CONFIG_SERVICE_CLASS_NAME = "DEVICE_SETTINGS_CONFIG_CLASS"
const val CONFIG_SERVICE_INTENT_ACTION = "DEVICE_SETTINGS_CONFIG_ACTION"
-
- val services =
- ConcurrentHashMap<
- EndPoint,
- StateFlow<ServiceConnectionStatus<IDeviceSettingsProviderService>>,
- >()
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 712ddc8..5eeb49a 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -167,6 +167,13 @@
return this;
}
+ public TestModeBuilder setVisualEffect(int effect, boolean allowed) {
+ ZenPolicy newPolicy = new ZenPolicy.Builder(mRule.getZenPolicy())
+ .showVisualEffect(effect, allowed).build();
+ setZenPolicy(newPolicy);
+ return this;
+ }
+
public TestModeBuilder setEnabled(boolean enabled) {
return setEnabled(enabled, /* byUser= */ false);
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
index 0cb6bc1..7a627c9 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt
@@ -53,7 +53,6 @@
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -140,11 +139,6 @@
)
}
- @After
- fun clean() {
- DeviceSettingServiceConnection.services.clear()
- }
-
@Test
fun getDeviceSettingsConfig_withMetadata_success() {
testScope.runTest {
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index d710939..5e31da4 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -164,6 +164,7 @@
Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS,
Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS,
Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+ Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
Settings.Secure.SHOW_NOTIFICATION_SNOOZE,
Settings.Secure.NOTIFICATION_HISTORY_ENABLED,
Settings.Secure.ZEN_DURATION,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index fa16a44..b3f7374 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -243,6 +243,7 @@
VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.SHOW_NOTIFICATION_SNOOZE, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.NOTIFICATION_HISTORY_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ZEN_DURATION, ANY_INTEGER_VALIDATOR);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index f8383d9..88cc152 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -40,16 +40,6 @@
}
flag {
- name: "notification_minimalism_prototype"
- namespace: "systemui"
- description: "Prototype of notification minimalism; the new 'Intermediate' lockscreen customization proposal."
- bug: "330387368"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "notification_view_flipper_pausing_v2"
namespace: "systemui"
description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed."
diff --git a/packages/SystemUI/animation/lib/Android.bp b/packages/SystemUI/animation/lib/Android.bp
index 4324d463..d9230ec 100644
--- a/packages/SystemUI/animation/lib/Android.bp
+++ b/packages/SystemUI/animation/lib/Android.bp
@@ -33,6 +33,20 @@
],
}
+// This is the core animation library written in java and can be depended by java sdk libraries.
+// Please don't introduce kotlin code in this target since kotlin is incompatible with sdk
+// libraries.
+java_library {
+ name: "PlatformAnimationLib-core",
+ srcs: [
+ "src/com/android/systemui/animation/*.java",
+ ":PlatformAnimationLib-aidl",
+ ],
+ static_libs: [
+ "WindowManager-Shell-shared",
+ ],
+}
+
filegroup {
name: "PlatformAnimationLib-aidl",
srcs: [
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
new file mode 100644
index 0000000..64bedd3
--- /dev/null
+++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java
@@ -0,0 +1,281 @@
+/*
+ * 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.animation;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.app.ActivityOptions.LaunchCookie;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.RemoteException;
+import android.util.Log;
+import android.window.IRemoteTransition;
+import android.window.RemoteTransition;
+
+import com.android.systemui.animation.shared.IOriginTransitions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * A session object that holds origin transition states for starting an activity from an on-screen
+ * UI component and smoothly transitioning back from the activity to the same UI component.
+ */
+public class OriginTransitionSession {
+ private static final String TAG = "OriginTransitionSession";
+ static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {NOT_STARTED, STARTED, CANCELLED})
+ private @interface State {}
+
+ @State private static final int NOT_STARTED = 0;
+ @State private static final int STARTED = 1;
+ @State private static final int CANCELLED = 5;
+
+ private final String mName;
+ @Nullable private final IOriginTransitions mOriginTransitions;
+ private final Predicate<RemoteTransition> mIntentStarter;
+ @Nullable private final IRemoteTransition mEntryTransition;
+ @Nullable private final IRemoteTransition mExitTransition;
+ private final AtomicInteger mState = new AtomicInteger(NOT_STARTED);
+
+ @Nullable private RemoteTransition mOriginTransition;
+
+ private OriginTransitionSession(
+ String name,
+ @Nullable IOriginTransitions originTransitions,
+ Predicate<RemoteTransition> intentStarter,
+ @Nullable IRemoteTransition entryTransition,
+ @Nullable IRemoteTransition exitTransition) {
+ mName = name;
+ mOriginTransitions = originTransitions;
+ mIntentStarter = intentStarter;
+ mEntryTransition = entryTransition;
+ mExitTransition = exitTransition;
+ if (hasExitTransition() && !hasEntryTransition()) {
+ throw new IllegalArgumentException(
+ "Entry transition must be supplied if you want to play an exit transition!");
+ }
+ }
+
+ /**
+ * Launch the target intent with the supplied entry transition. After this method, the entry
+ * transition is expected to receive callbacks. The exit transition will be registered and
+ * triggered when the system detects a return from the launched activity to the launching
+ * activity.
+ */
+ public boolean start() {
+ if (!mState.compareAndSet(NOT_STARTED, STARTED)) {
+ logE("start: illegal state - " + stateToString(mState.get()));
+ return false;
+ }
+
+ RemoteTransition remoteTransition = null;
+ if (hasEntryTransition() && hasExitTransition()) {
+ logD("start: starting with entry and exit transition.");
+ try {
+ remoteTransition =
+ mOriginTransition =
+ mOriginTransitions.makeOriginTransition(
+ new RemoteTransition(mEntryTransition, mName + "-entry"),
+ new RemoteTransition(mExitTransition, mName + "-exit"));
+ } catch (RemoteException e) {
+ logE("Unable to create origin transition!", e);
+ }
+ } else if (hasEntryTransition()) {
+ logD("start: starting with entry transition.");
+ remoteTransition = new RemoteTransition(mEntryTransition, mName + "-entry");
+
+ } else {
+ logD("start: starting without transition.");
+ }
+ if (mIntentStarter.test(remoteTransition)) {
+ return true;
+ } else {
+ // Animation is cancelled by intent starter.
+ logD("start: cancelled by intent starter!");
+ cancel();
+ return false;
+ }
+ }
+
+ /**
+ * Cancel the current transition and the registered exit transition if it exists. After this
+ * method, this session object can no longer be used. Clients need to create a new session
+ * object if they want to launch another intent with origin transition.
+ */
+ public void cancel() {
+ final int lastState = mState.getAndSet(CANCELLED);
+ if (lastState == CANCELLED || lastState == NOT_STARTED) {
+ return;
+ }
+ logD("cancel: cancelled transition. Last state: " + stateToString(lastState));
+ if (mOriginTransition != null) {
+ try {
+ mOriginTransitions.cancelOriginTransition(mOriginTransition);
+ mOriginTransition = null;
+ } catch (RemoteException e) {
+ logE("Unable to cancel origin transition!", e);
+ }
+ }
+ }
+
+ private boolean hasEntryTransition() {
+ return mEntryTransition != null;
+ }
+
+ private boolean hasExitTransition() {
+ return mOriginTransitions != null && mExitTransition != null;
+ }
+
+ private void logD(String msg) {
+ if (DEBUG) {
+ Log.d(TAG, "Session[" + mName + "] - " + msg);
+ }
+ }
+
+ private void logE(String msg) {
+ Log.e(TAG, "Session[" + mName + "] - " + msg);
+ }
+
+ private void logE(String msg, Throwable e) {
+ Log.e(TAG, "Session[" + mName + "] - " + msg, e);
+ }
+
+ private static String stateToString(@State int state) {
+ switch (state) {
+ case NOT_STARTED:
+ return "NOT_STARTED";
+ case STARTED:
+ return "STARTED";
+ case CANCELLED:
+ return "CANCELLED";
+ default:
+ return "UNKNOWN(" + state + ")";
+ }
+ }
+
+ /** A builder to build a {@link OriginTransitionSession}. */
+ public static class Builder {
+ private final Context mContext;
+ @Nullable private final IOriginTransitions mOriginTransitions;
+ @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier;
+ @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier;
+ private String mName;
+ @Nullable private Predicate<RemoteTransition> mIntentStarter;
+
+ /** Create a builder that only supports entry transition. */
+ public Builder(Context context) {
+ this(context, /* originTransitions= */ null);
+ }
+
+ /** Create a builder that supports both entry and return transition. */
+ public Builder(Context context, @Nullable IOriginTransitions originTransitions) {
+ mContext = context;
+ mOriginTransitions = originTransitions;
+ mName = context.getPackageName();
+ }
+
+ /** Specify a name that is used in logging. */
+ public Builder withName(String name) {
+ mName = name;
+ return this;
+ }
+
+ /** Specify an intent that will be launched when the session started. */
+ public Builder withIntent(Intent intent) {
+ return withIntentStarter(
+ transition -> {
+ mContext.startActivity(
+ intent, createDefaultActivityOptions(transition).toBundle());
+ return true;
+ });
+ }
+
+ /** Specify a pending intent that will be launched when the session started. */
+ public Builder withPendingIntent(PendingIntent pendingIntent) {
+ return withIntentStarter(
+ transition -> {
+ try {
+ pendingIntent.send(createDefaultActivityOptions(transition).toBundle());
+ return true;
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, "Failed to launch pending intent!", e);
+ return false;
+ }
+ });
+ }
+
+ private static ActivityOptions createDefaultActivityOptions(
+ @Nullable RemoteTransition transition) {
+ ActivityOptions options =
+ transition == null
+ ? ActivityOptions.makeBasic()
+ : ActivityOptions.makeRemoteTransition(transition);
+ LaunchCookie cookie = new LaunchCookie();
+ options.setLaunchCookie(cookie);
+ return options;
+ }
+
+ /**
+ * Specify an intent starter function that will be called to start an activity. The function
+ * accepts an optional {@link RemoteTransition} object which can be used to create an {@link
+ * ActivityOptions} for the activity launch. The function can also return a {@code false}
+ * result to cancel the session.
+ *
+ * <p>Note: it's encouraged to use {@link #withIntent(Intent)} or {@link
+ * #withPendingIntent(PendingIntent)} instead of this method. Use it only if the default
+ * activity launch code doesn't satisfy your requirement.
+ */
+ public Builder withIntentStarter(Predicate<RemoteTransition> intentStarter) {
+ mIntentStarter = intentStarter;
+ return this;
+ }
+
+ /** Add an entry transition to the builder. */
+ public Builder withEntryTransition(IRemoteTransition transition) {
+ mEntryTransitionSupplier = () -> transition;
+ return this;
+ }
+
+ /** Add an exit transition to the builder. */
+ public Builder withExitTransition(IRemoteTransition transition) {
+ mExitTransitionSupplier = () -> transition;
+ return this;
+ }
+
+ /** Build an {@link OriginTransitionSession}. */
+ public OriginTransitionSession build() {
+ if (mIntentStarter == null) {
+ throw new IllegalArgumentException("No intent, pending intent, or intent starter!");
+ }
+ return new OriginTransitionSession(
+ mName,
+ mOriginTransitions,
+ mIntentStarter,
+ mEntryTransitionSupplier == null ? null : mEntryTransitionSupplier.get(),
+ mExitTransitionSupplier == null ? null : mExitTransitionSupplier.get());
+ }
+ }
+}
diff --git a/packages/SystemUI/animation/lib/tests/Android.bp b/packages/SystemUI/animation/lib/tests/Android.bp
new file mode 100644
index 0000000..c1a3e84
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/Android.bp
@@ -0,0 +1,47 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
+ default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_test {
+ name: "PlatformAnimationLibCoreTests",
+
+ defaults: [
+ "platform_app_defaults",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+
+ dxflags: ["--multi-dex"],
+ platform_apis: true,
+ test_suites: ["device-tests"],
+ static_libs: [
+ "PlatformAnimationLib-core",
+ "platform-test-rules",
+ "testables",
+ ],
+ compile_multilib: "both",
+ libs: [
+ "android.test.runner.stubs.system",
+ "android.test.base.stubs.system",
+ ],
+
+ certificate: "platform",
+
+ manifest: "AndroidManifest.xml",
+}
diff --git a/packages/SystemUI/animation/lib/tests/AndroidManifest.xml b/packages/SystemUI/animation/lib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..1788abf
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:sharedUserId="android.uid.system"
+ package="com.android.systemui.animation.core.tests" >
+
+ <application android:debuggable="true" android:testOnly="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="android.testing.TestableInstrumentation"
+ android:targetPackage="com.android.systemui.animation.core.tests"
+ android:label="Tests for PlatformAnimationLib-core" />
+</manifest>
diff --git a/packages/SystemUI/animation/lib/tests/AndroidTest.xml b/packages/SystemUI/animation/lib/tests/AndroidTest.xml
new file mode 100644
index 0000000..0f37d7a
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/AndroidTest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<configuration description="Runs Tests for PlatformAnimationLib-core.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="PlatformAnimationLibCoreTests.apk" />
+ <option name="install-arg" value="-t" />
+ </target_preparer>
+
+ <!-- Among other reasons, root access is needed for screen recording artifacts. -->
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+ <option name="force-root" value="true" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.systemui.animation.core.tests" />
+ <option name="runner" value="android.testing.TestableInstrumentation" />
+ <option name="test-filter-dir" value="/data/data/com.android.systemui.animation.core.tests" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/data/user/0/com.android.systemui.animation.core.tests/files" />
+ <option name="collect-on-run-ended-only" value="false" />
+ </metrics_collector>
+</configuration>
diff --git a/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java b/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java
new file mode 100644
index 0000000..287e53b
--- /dev/null
+++ b/packages/SystemUI/animation/lib/tests/src/com/android/systemui/animation/OriginTransitionSessionTest.java
@@ -0,0 +1,406 @@
+/*
+ * 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.animation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.annotation.Nullable;
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.IRemoteTransition;
+import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransition;
+import android.window.TransitionInfo;
+import android.window.WindowAnimationState;
+import android.window.WindowContainerTransaction;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.systemui.animation.shared.IOriginTransitions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Map;
+import java.util.function.Predicate;
+
+/** Unit tests for {@link OriginTransitionSession}. */
+@SmallTest
+@RunWith(JUnit4.class)
+public final class OriginTransitionSessionTest {
+ private static final ComponentName TEST_ACTIVITY_1 = new ComponentName("test", "Activity1");
+ private static final ComponentName TEST_ACTIVITY_2 = new ComponentName("test", "Activity2");
+ private static final ComponentName TEST_ACTIVITY_3 = new ComponentName("test", "Activity3");
+
+ private FakeIOriginTransitions mIOriginTransitions;
+ private Instrumentation mInstrumentation;
+ private FakeIntentStarter mIntentStarter;
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mContext = mInstrumentation.getTargetContext();
+ mIOriginTransitions = new FakeIOriginTransitions();
+ mIntentStarter = new FakeIntentStarter(TEST_ACTIVITY_1, TEST_ACTIVITY_2);
+ }
+
+ @Test
+ public void sessionStart_withEntryAndExitTransition_transitionsPlayed() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ FakeRemoteTransition exit = new FakeRemoteTransition();
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .withExitTransition(exit)
+ .build();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isTrue();
+ assertThat(entry.started()).isTrue();
+
+ runReturnTransition(mIntentStarter);
+
+ assertThat(exit.started()).isTrue();
+ }
+
+ @Test
+ public void sessionStart_withEntryTransition_transitionPlayed() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .build();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isTrue();
+ assertThat(entry.started()).isTrue();
+ assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+ }
+
+ @Test
+ public void sessionStart_withoutTransition_launchedIntent() {
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .build();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isTrue();
+ assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+ }
+
+ @Test
+ public void sessionStart_cancelledByIntentStarter_transitionNotPlayed() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ FakeRemoteTransition exit = new FakeRemoteTransition();
+ mIntentStarter =
+ new FakeIntentStarter(TEST_ACTIVITY_1, TEST_ACTIVITY_2, /* result= */ false);
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .withExitTransition(exit)
+ .build();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isFalse();
+ assertThat(entry.started()).isFalse();
+ assertThat(exit.started()).isFalse();
+ assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+ }
+
+ @Test
+ public void sessionStart_alreadyStarted_noOp() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ FakeRemoteTransition exit = new FakeRemoteTransition();
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .withExitTransition(exit)
+ .build();
+ session.start();
+ entry.reset();
+ mIntentStarter.reset();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isFalse();
+ assertThat(entry.started()).isFalse();
+ }
+
+ @Test
+ public void sessionStart_alreadyCancelled_noOp() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ FakeRemoteTransition exit = new FakeRemoteTransition();
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .withExitTransition(exit)
+ .build();
+ session.cancel();
+
+ session.start();
+
+ assertThat(mIntentStarter.hasLaunched()).isFalse();
+ assertThat(entry.started()).isFalse();
+ assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+ }
+
+ @Test
+ public void sessionCancelled_returnTransitionNotPlayed() {
+ FakeRemoteTransition entry = new FakeRemoteTransition();
+ FakeRemoteTransition exit = new FakeRemoteTransition();
+ OriginTransitionSession session =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(mIntentStarter)
+ .withEntryTransition(entry)
+ .withExitTransition(exit)
+ .build();
+
+ session.start();
+ session.cancel();
+
+ assertThat(mIOriginTransitions.hasPendingReturnTransitions()).isFalse();
+ }
+
+ @Test
+ public void multipleSessionsStarted_allTransitionsPlayed() {
+ FakeRemoteTransition entry1 = new FakeRemoteTransition();
+ FakeRemoteTransition exit1 = new FakeRemoteTransition();
+ FakeIntentStarter starter1 = mIntentStarter;
+ OriginTransitionSession session1 =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(starter1)
+ .withEntryTransition(entry1)
+ .withExitTransition(exit1)
+ .build();
+ FakeRemoteTransition entry2 = new FakeRemoteTransition();
+ FakeRemoteTransition exit2 = new FakeRemoteTransition();
+ FakeIntentStarter starter2 = new FakeIntentStarter(TEST_ACTIVITY_2, TEST_ACTIVITY_3);
+ OriginTransitionSession session2 =
+ new OriginTransitionSession.Builder(mContext, mIOriginTransitions)
+ .withIntentStarter(starter2)
+ .withEntryTransition(entry2)
+ .withExitTransition(exit2)
+ .build();
+
+ session1.start();
+
+ assertThat(starter1.hasLaunched()).isTrue();
+ assertThat(entry1.started()).isTrue();
+
+ session2.start();
+
+ assertThat(starter2.hasLaunched()).isTrue();
+ assertThat(entry2.started()).isTrue();
+
+ runReturnTransition(starter2);
+
+ assertThat(exit2.started()).isTrue();
+
+ runReturnTransition(starter1);
+
+ assertThat(exit1.started()).isTrue();
+ }
+
+ private void runReturnTransition(FakeIntentStarter intentStarter) {
+ TransitionInfo info =
+ buildTransitionInfo(intentStarter.getToActivity(), intentStarter.getFromActivity());
+ mIOriginTransitions.runReturnTransition(intentStarter.getTransitionOfLastLaunch(), info);
+ }
+
+ private static TransitionInfo buildTransitionInfo(ComponentName from, ComponentName to) {
+ TransitionInfo info = new TransitionInfo(WindowManager.TRANSIT_OPEN, /* flags= */ 0);
+ TransitionInfo.Change c1 =
+ new TransitionInfo.Change(/* container= */ null, /* leash= */ null);
+ c1.setMode(WindowManager.TRANSIT_OPEN);
+ c1.setActivityComponent(to);
+ TransitionInfo.Change c2 =
+ new TransitionInfo.Change(/* container= */ null, /* leash= */ null);
+ c2.setMode(WindowManager.TRANSIT_CLOSE);
+ c2.setActivityComponent(from);
+ info.addChange(c2);
+ info.addChange(c1);
+ return info;
+ }
+
+ private static class FakeIntentStarter implements Predicate<RemoteTransition> {
+ private final ComponentName mFromActivity;
+ private final ComponentName mToActivity;
+ private final boolean mResult;
+
+ @Nullable private RemoteTransition mTransition;
+ private boolean mLaunched;
+
+ FakeIntentStarter(ComponentName from, ComponentName to) {
+ this(from, to, /* result= */ true);
+ }
+
+ FakeIntentStarter(ComponentName from, ComponentName to, boolean result) {
+ mFromActivity = from;
+ mToActivity = to;
+ mResult = result;
+ }
+
+ @Override
+ public boolean test(RemoteTransition transition) {
+ if (mResult) {
+ mLaunched = true;
+ mTransition = transition;
+ if (mTransition != null) {
+ TransitionInfo info = buildTransitionInfo(mFromActivity, mToActivity);
+ try {
+ transition
+ .getRemoteTransition()
+ .startAnimation(
+ new Binder(),
+ info,
+ new SurfaceControl.Transaction(),
+ new FakeFinishCallback());
+ } catch (RemoteException e) {
+
+ }
+ }
+ }
+ return mResult;
+ }
+
+ @Nullable
+ public RemoteTransition getTransitionOfLastLaunch() {
+ return mTransition;
+ }
+
+ public ComponentName getFromActivity() {
+ return mFromActivity;
+ }
+
+ public ComponentName getToActivity() {
+ return mToActivity;
+ }
+
+ public boolean hasLaunched() {
+ return mLaunched;
+ }
+
+ public void reset() {
+ mTransition = null;
+ mLaunched = false;
+ }
+ }
+
+ private static class FakeIOriginTransitions extends IOriginTransitions.Stub {
+ private final Map<RemoteTransition, RemoteTransition> mRecords = new ArrayMap<>();
+
+ @Override
+ public RemoteTransition makeOriginTransition(
+ RemoteTransition launchTransition, RemoteTransition returnTransition) {
+ mRecords.put(launchTransition, returnTransition);
+ return launchTransition;
+ }
+
+ @Override
+ public void cancelOriginTransition(RemoteTransition originTransition) {
+ mRecords.remove(originTransition);
+ }
+
+ public void runReturnTransition(RemoteTransition originTransition, TransitionInfo info) {
+ RemoteTransition transition = mRecords.remove(originTransition);
+ try {
+ transition
+ .getRemoteTransition()
+ .startAnimation(
+ new Binder(),
+ info,
+ new SurfaceControl.Transaction(),
+ new FakeFinishCallback());
+ } catch (RemoteException e) {
+
+ }
+ }
+
+ public boolean hasPendingReturnTransitions() {
+ return !mRecords.isEmpty();
+ }
+ }
+
+ private static class FakeFinishCallback extends IRemoteTransitionFinishedCallback.Stub {
+ @Override
+ public void onTransitionFinished(
+ WindowContainerTransaction wct, SurfaceControl.Transaction sct) {}
+ }
+
+ private static class FakeRemoteTransition extends IRemoteTransition.Stub {
+ private boolean mStarted;
+
+ @Override
+ public void startAnimation(
+ IBinder token,
+ TransitionInfo info,
+ SurfaceControl.Transaction t,
+ IRemoteTransitionFinishedCallback finishCallback)
+ throws RemoteException {
+ mStarted = true;
+ finishCallback.onTransitionFinished(null, null);
+ }
+
+ @Override
+ public void mergeAnimation(
+ IBinder transition,
+ TransitionInfo info,
+ SurfaceControl.Transaction t,
+ IBinder mergeTarget,
+ IRemoteTransitionFinishedCallback finishCallback) {}
+
+ @Override
+ public void takeOverAnimation(
+ IBinder transition,
+ TransitionInfo info,
+ SurfaceControl.Transaction t,
+ IRemoteTransitionFinishedCallback finishCallback,
+ WindowAnimationState[] states) {}
+
+ @Override
+ public void onTransitionConsumed(IBinder transition, boolean aborted) {}
+
+ public boolean started() {
+ return mStarted;
+ }
+
+ public void reset() {
+ mStarted = false;
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
index bfeaf92..0f6e6a7 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/LockscreenSceneModule.kt
@@ -28,6 +28,7 @@
import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.scene.ui.composable.Scene
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -35,12 +36,7 @@
import javax.inject.Provider
import kotlinx.coroutines.ExperimentalCoroutinesApi
-@Module(
- includes =
- [
- LockscreenSceneBlueprintModule::class,
- ],
-)
+@Module(includes = [LockscreenSceneBlueprintModule::class])
interface LockscreenSceneModule {
@Binds @IntoSet fun lockscreenScene(scene: LockscreenScene): Scene
@@ -51,9 +47,7 @@
@Provides
@SysUISingleton
@KeyguardRootView
- fun viewProvider(
- configurator: Provider<KeyguardViewConfigurator>,
- ): () -> View {
+ fun viewProvider(configurator: Provider<KeyguardViewConfigurator>): () -> View {
return { configurator.get().getKeyguardRootView() }
}
@@ -67,10 +61,16 @@
@Provides
fun providesLockscreenContent(
viewModelFactory: LockscreenContentViewModel.Factory,
+ notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
clockInteractor: KeyguardClockInteractor,
): LockscreenContent {
- return LockscreenContent(viewModelFactory, blueprints, clockInteractor)
+ return LockscreenContent(
+ viewModelFactory,
+ notificationScrimViewModelFactory,
+ blueprints,
+ clockInteractor,
+ )
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index dbe7538..5c5514a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -30,6 +30,8 @@
import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.notifications.ui.composable.NotificationLockscreenScrim
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
/**
* Renders the content of the lockscreen.
@@ -39,6 +41,7 @@
*/
class LockscreenContent(
private val viewModelFactory: LockscreenContentViewModel.Factory,
+ private val notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
private val blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
private val clockInteractor: KeyguardClockInteractor,
) {
@@ -47,10 +50,13 @@
}
@Composable
- fun SceneScope.Content(
- modifier: Modifier = Modifier,
- ) {
- val viewModel = rememberViewModel("LockscreenContent") { viewModelFactory.create() }
+ fun SceneScope.Content(modifier: Modifier = Modifier) {
+ val viewModel =
+ rememberViewModel("LockscreenContent-viewModel") { viewModelFactory.create() }
+ val notificationLockscreenScrimViewModel =
+ rememberViewModel("LockscreenContent-scrimViewModel") {
+ notificationScrimViewModelFactory.create()
+ }
val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle()
if (!isContentVisible) {
// If the content isn't supposed to be visible, show a large empty box as it's needed
@@ -71,6 +77,9 @@
}
val blueprint = blueprintByBlueprintId[blueprintId] ?: return
- with(blueprint) { Content(viewModel, modifier.sysuiResTag("keyguard_root_view")) }
+ with(blueprint) {
+ Content(viewModel, modifier.sysuiResTag("keyguard_root_view"))
+ NotificationLockscreenScrim(notificationLockscreenScrimViewModel)
+ }
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt
new file mode 100644
index 0000000..4279be3
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationLockscreenScrim.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.notifications.ui.composable
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
+import kotlinx.coroutines.launch
+
+/**
+ * A full-screen notifications scrim that is only visible after transitioning from Shade scene to
+ * Lockscreen Scene and ending user input, at which point it fades out, visually completing the
+ * transition.
+ */
+@Composable
+fun SceneScope.NotificationLockscreenScrim(
+ viewModel: NotificationLockscreenScrimViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val shadeMode = viewModel.shadeMode.collectAsStateWithLifecycle()
+
+ // Important: Make sure that shouldShowScrimFadeOut() is checked the first time the Lockscreen
+ // scene is composed.
+ val useFadeOutOnComposition =
+ remember(shadeMode.value) {
+ layoutState.currentTransition?.let { currentTransition ->
+ shouldShowScrimFadeOut(currentTransition, shadeMode.value)
+ } ?: false
+ }
+
+ val alphaAnimatable = remember { Animatable(1f) }
+
+ LaunchedEffect(
+ alphaAnimatable,
+ layoutState.currentTransition,
+ useFadeOutOnComposition,
+ shadeMode,
+ ) {
+ val currentTransition = layoutState.currentTransition
+ if (
+ useFadeOutOnComposition &&
+ currentTransition != null &&
+ shouldShowScrimFadeOut(currentTransition, shadeMode.value) &&
+ currentTransition.isUserInputOngoing
+ ) {
+ // keep scrim visible until user lifts their finger.
+ viewModel.setAlphaForLockscreenFadeIn(0f)
+ alphaAnimatable.snapTo(1f)
+ } else if (
+ useFadeOutOnComposition &&
+ (currentTransition == null ||
+ (shouldShowScrimFadeOut(currentTransition, shadeMode.value) &&
+ !currentTransition.isUserInputOngoing))
+ ) {
+ // we no longer want to keep the scrim from fading out, so animate the scrim fade-out
+ // and pipe the progress to the view model as well, so NSSL can fade-in the stack in
+ // tandem.
+ viewModel.setAlphaForLockscreenFadeIn(0f)
+ coroutineScope.launch {
+ snapshotFlow { alphaAnimatable.value }
+ .collect { viewModel.setAlphaForLockscreenFadeIn(1 - it) }
+ }
+ alphaAnimatable.animateTo(0f, tween())
+ } else {
+ // disable the scrim fade logic.
+ viewModel.setAlphaForLockscreenFadeIn(1f)
+ alphaAnimatable.snapTo(0f)
+ }
+ }
+
+ Box(
+ modifier
+ .fillMaxSize()
+ .element(Notifications.Elements.NotificationScrim)
+ .graphicsLayer { alpha = alphaAnimatable.value }
+ .background(MaterialTheme.colorScheme.surface)
+ )
+}
+
+private fun shouldShowScrimFadeOut(
+ currentTransition: TransitionState.Transition,
+ shadeMode: ShadeMode,
+): Boolean {
+ return shadeMode == ShadeMode.Single &&
+ currentTransition.isInitiatedByUserInput &&
+ (currentTransition.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen) ||
+ currentTransition.isTransitioning(from = Scenes.Bouncer, to = Scenes.Lockscreen))
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index fe4a65b..2066c93 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -86,6 +86,8 @@
import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.NestedScrollBehavior
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.SceneTransitionLayoutState
+import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.thenIf
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.res.R
@@ -101,7 +103,7 @@
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
object Notifications {
@@ -251,6 +253,7 @@
NotificationPlaceholder(
stackScrollView = stackScrollView,
viewModel = viewModel,
+ useStackBounds = { shouldUseLockscreenStackBounds(layoutState.transitionState) },
modifier = Modifier.fillMaxSize(),
)
HeadsUpNotificationSpace(
@@ -363,7 +366,6 @@
snapshotFlow { syntheticScroll.value }
.collect { delta ->
scrollNotificationStack(
- scope = coroutineScope,
delta = delta,
animate = false,
scrimOffset = scrimOffset,
@@ -383,7 +385,6 @@
// composed at least once), and our remote input row overlaps with the ime bounds.
if (isRemoteInputActive && imeTopValue > 0f && remoteInputRowBottom > imeTopValue) {
scrollNotificationStack(
- scope = coroutineScope,
delta = remoteInputRowBottom - imeTopValue,
animate = true,
scrimOffset = scrimOffset,
@@ -450,7 +451,10 @@
scrimCornerRadius,
screenCornerRadius,
{ expansionFraction },
- shouldPunchHoleBehindScrim,
+ shouldAnimateScrimCornerRadius(
+ layoutState,
+ shouldPunchHoleBehindScrim,
+ ),
)
.let { scrimRounding.value.toRoundedCornerShape(it) }
clip = true
@@ -514,6 +518,9 @@
NotificationPlaceholder(
stackScrollView = stackScrollView,
viewModel = viewModel,
+ useStackBounds = {
+ !shouldUseLockscreenStackBounds(layoutState.transitionState)
+ },
modifier =
Modifier.notificationStackHeight(
view = stackScrollView,
@@ -600,6 +607,7 @@
private fun SceneScope.NotificationPlaceholder(
stackScrollView: NotificationScrollView,
viewModel: NotificationsPlaceholderViewModel,
+ useStackBounds: () -> Boolean,
modifier: Modifier = Modifier,
) {
Box(
@@ -609,21 +617,26 @@
.debugBackground(viewModel, DEBUG_STACK_COLOR)
.onSizeChanged { size -> debugLog(viewModel) { "STACK onSizeChanged: size=$size" } }
.onGloballyPositioned { coordinates: LayoutCoordinates ->
- val positionInWindow = coordinates.positionInWindow()
- debugLog(viewModel) {
- "STACK onGloballyPositioned:" +
- " size=${coordinates.size}" +
- " position=$positionInWindow" +
- " bounds=${coordinates.boundsInWindow()}"
+ // This element is opted out of the shared element system, so there can be
+ // multiple instances of it during a transition. Thus we need to determine which
+ // instance should feed its bounds to NSSL to avoid providing conflicting values
+ val useBounds = useStackBounds()
+ if (useBounds) {
+ // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top won't
+ val positionInWindow = coordinates.positionInWindow()
+ debugLog(viewModel) {
+ "STACK onGloballyPositioned:" +
+ " size=${coordinates.size}" +
+ " position=$positionInWindow" +
+ " bounds=${coordinates.boundsInWindow()}"
+ }
+ stackScrollView.setStackTop(positionInWindow.y)
}
- // NOTE: positionInWindow.y scrolls off screen, but boundsInWindow.top will not
- stackScrollView.setStackTop(positionInWindow.y)
}
)
}
private suspend fun scrollNotificationStack(
- scope: CoroutineScope,
delta: Float,
animate: Boolean,
scrimOffset: Animatable<Float, AnimationVector1D>,
@@ -638,7 +651,7 @@
if (animate) {
// launch a new coroutine for the remainder animation so that it doesn't suspend the
// scrim animation, allowing both to play simultaneously.
- scope.launch { scrollState.animateScrollTo(remainingDelta) }
+ coroutineScope { launch { scrollState.animateScrollTo(remainingDelta) } }
} else {
scrollState.scrollTo(remainingDelta)
}
@@ -658,6 +671,18 @@
}
}
+private fun shouldUseLockscreenStackBounds(state: TransitionState): Boolean {
+ return state is TransitionState.Idle && state.currentScene == Scenes.Lockscreen
+}
+
+private fun shouldAnimateScrimCornerRadius(
+ state: SceneTransitionLayoutState,
+ shouldPunchHoleBehindScrim: Boolean,
+): Boolean {
+ return shouldPunchHoleBehindScrim ||
+ state.isTransitioning(from = Scenes.Shade, to = Scenes.Lockscreen)
+}
+
private fun calculateCornerRadius(
scrimCornerRadius: Dp,
screenCornerRadius: Dp,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index df101c5..dc9e267 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -126,17 +126,18 @@
awaitFirstDown(false)
viewModel.onSceneContainerUserInputStarted()
}
- },
+ }
) {
SceneTransitionLayout(
state = state,
modifier = modifier.fillMaxSize(),
swipeSourceDetector = viewModel.edgeDetector,
+ gestureFilter = viewModel::shouldFilterGesture,
) {
sceneByKey.forEach { (sceneKey, scene) ->
scene(
key = sceneKey,
- userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap())
+ userActions = userActionsByContentKey.getOrDefault(sceneKey, emptyMap()),
) {
// Activate the scene.
LaunchedEffect(scene) { scene.activate() }
@@ -144,7 +145,7 @@
// Render the scene.
with(scene) {
this@scene.Content(
- modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize(),
+ modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize()
)
}
}
@@ -152,7 +153,7 @@
overlayByKey.forEach { (overlayKey, overlay) ->
overlay(
key = overlayKey,
- userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap())
+ userActions = userActionsByContentKey.getOrDefault(overlayKey, emptyMap()),
) {
// Activate the overlay.
LaunchedEffect(overlay) { overlay.activate() }
@@ -164,12 +165,7 @@
}
BottomRightCornerRibbon(
- content = {
- Text(
- text = "flexi\uD83E\uDD43",
- color = Color.White,
- )
- },
+ content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) },
modifier = Modifier.align(Alignment.BottomEnd),
)
}
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 58fbf43..303a6f0 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
@@ -82,18 +82,35 @@
sharedElement(Notifications.Elements.HeadsUpNotificationPlaceholder, enabled = false)
}
from(Scenes.Shade, to = Scenes.QuickSettings) { shadeToQuickSettingsTransition() }
+ from(Scenes.Shade, to = Scenes.Lockscreen) {
+ reversed { lockscreenToShadeTransition() }
+ sharedElement(Notifications.Elements.NotificationStackPlaceholder, enabled = false)
+ }
// Overlay transitions
to(Overlays.NotificationsShade) { toNotificationsShadeTransition() }
to(Overlays.QuickSettingsShade) { toQuickSettingsShadeTransition() }
- from(Overlays.NotificationsShade, Overlays.QuickSettingsShade) {
+ from(Overlays.NotificationsShade, to = Overlays.QuickSettingsShade) {
notificationsShadeToQuickSettingsShadeTransition()
}
+ from(Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) {
+ toNotificationsShadeTransition(durationScale = 0.9)
+ }
+ from(Scenes.Gone, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) {
+ toQuickSettingsShadeTransition(durationScale = 0.9)
+ }
+ from(Scenes.Lockscreen, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) {
+ toNotificationsShadeTransition(durationScale = 0.9)
+ }
+ from(Scenes.Lockscreen, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) {
+ toQuickSettingsShadeTransition(durationScale = 0.9)
+ }
// Scene overscroll
overscrollDisabled(Scenes.Gone, Orientation.Vertical)
+ overscrollDisabled(Scenes.Lockscreen, Orientation.Vertical)
overscroll(Scenes.Bouncer, Orientation.Vertical) {
translate(Bouncer.Elements.Content, y = { absoluteDistance })
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
index ac54896..4c0efd2 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromLockscreenToBouncerTransition.kt
@@ -4,13 +4,19 @@
import androidx.compose.animation.core.tween
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.TransitionBuilder
+import com.android.compose.animation.scene.UserActionDistance
import com.android.systemui.bouncer.ui.composable.Bouncer
const val FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION = 0.5f
+const val FROM_LOCK_SCREEN_TO_BOUNCER_SWIPE_DISTANCE_FRACTION = 0.5f
fun TransitionBuilder.lockscreenToBouncerTransition() {
spec = tween(durationMillis = 500)
+ distance = UserActionDistance { fromSceneSize, _ ->
+ fromSceneSize.height * FROM_LOCK_SCREEN_TO_BOUNCER_SWIPE_DISTANCE_FRACTION
+ }
+
translate(Bouncer.Elements.Content, y = 300.dp)
fractionRange(end = FROM_LOCK_SCREEN_TO_BOUNCER_FADE_FRACTION) {
fade(Bouncer.Elements.Background)
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 7f2ee2a..db0fe3e 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
@@ -296,7 +296,7 @@
val shouldPunchHoleBehindScrim =
layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) ||
- layoutState.isTransitioningBetween(Scenes.Lockscreen, Scenes.Shade)
+ layoutState.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade)
// Media is visible and we are in landscape on a small height screen
val mediaInRow = isMediaVisible && isLandscape()
val mediaOffset by
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index f38a310..9891025 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -123,6 +123,10 @@
overSlop: Float,
pointersDown: Int,
): DragController {
+ if (startedPosition != null && layoutImpl.gestureFilter(startedPosition)) {
+ return NoOpDragController
+ }
+
if (overSlop == 0f) {
val oldDragController = dragController
check(oldDragController != null && oldDragController.isDrivingTransition) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index cec8883..6e89814 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -47,6 +47,9 @@
* @param state the state of this layout.
* @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from,
* if any.
+ * @param gestureFilter decides whether a drag gesture that started at the given start position
+ * should be filtered. If the lambda returns `true`, the drag gesture will be ignored. If it
+ * returns `false`, the drag gesture will be handled.
* @param transitionInterceptionThreshold used during a scene transition. For the scene to be
* intercepted, the progress value must be above the threshold, and below (1 - threshold).
* @param builder the configuration of the different scenes and overlays of this layout.
@@ -57,6 +60,7 @@
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
+ gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
builder: SceneTransitionLayoutScope.() -> Unit,
) {
@@ -65,6 +69,7 @@
modifier,
swipeSourceDetector,
swipeDetector,
+ gestureFilter,
transitionInterceptionThreshold,
onLayoutImpl = null,
builder,
@@ -616,6 +621,7 @@
modifier: Modifier = Modifier,
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
+ gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter,
transitionInterceptionThreshold: Float = 0f,
onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
builder: SceneTransitionLayoutScope.() -> Unit,
@@ -632,6 +638,7 @@
transitionInterceptionThreshold = transitionInterceptionThreshold,
builder = builder,
animationScope = animationScope,
+ gestureFilter = gestureFilter,
)
.also { onLayoutImpl?.invoke(it) }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 65c4043..9e7be37 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -31,6 +31,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LookaheadScope
@@ -70,6 +71,7 @@
* animations.
*/
internal val animationScope: CoroutineScope,
+ internal val gestureFilter: (startedPosition: Offset) -> Boolean,
) {
/**
* The map of [Scene]s.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
index 54ee783..f758102 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeDetector.kt
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene
import androidx.compose.runtime.Stable
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerInputChange
/** {@link SwipeDetector} helps determine whether a swipe gestured has occurred. */
@@ -31,6 +32,8 @@
val DefaultSwipeDetector = PassthroughSwipeDetector()
+val DefaultGestureFilter = { _: Offset -> false }
+
/** An {@link SwipeDetector} implementation that recognizes a swipe on any input. */
class PassthroughSwipeDetector : SwipeDetector {
override fun detectSwipe(change: PointerInputChange): Boolean {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index fca92ca..b64b8be 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -108,6 +108,8 @@
val transitionInterceptionThreshold = 0.05f
+ var gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter
+
private val layoutImpl =
SceneTransitionLayoutImpl(
state = layoutState,
@@ -120,6 +122,7 @@
// Use testScope and not backgroundScope here because backgroundScope does not
// work well with advanceUntilIdle(), which is used by some tests.
animationScope = testScope,
+ gestureFilter = { startedPosition -> gestureFilter.invoke(startedPosition) },
)
.apply { setContentsAndLayoutTargetSizeForTest(LAYOUT_SIZE) }
@@ -317,6 +320,13 @@
}
@Test
+ fun onDragStarted_doesNotStartTransition_whenGestureFiltered() = runGestureTest {
+ gestureFilter = { _ -> true }
+ onDragStarted(overSlop = down(fractionOfScreen = 0.1f), expectedConsumedOverSlop = 0f)
+ assertIdle(currentScene = SceneA)
+ }
+
+ @Test
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f))
assertTransition(currentScene = SceneA)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
index a08fbbf..fa304c9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
@@ -198,6 +198,13 @@
@DisableFlags(Flags.FLAG_SCENE_CONTAINER)
fun testTransitionToGlanceableHubOnWake() =
testScope.runTest {
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DREAMING,
+ testScope,
+ )
+ reset(transitionRepository)
+
whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
kosmos.setCommunalAvailable(true)
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
index ba68917..29035ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorTest.kt
@@ -38,7 +38,6 @@
import com.android.systemui.scene.data.repository.Idle
import com.android.systemui.scene.data.repository.Transition
import com.android.systemui.scene.data.repository.setSceneTransition
-import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.testKosmos
@@ -76,7 +75,6 @@
transitionInteractor = kosmos.keyguardTransitionInteractor,
dismissInteractor = dismissInteractor,
applicationScope = testScope.backgroundScope,
- sceneInteractor = { kosmos.sceneInteractor },
deviceUnlockedInteractor = { kosmos.deviceUnlockedInteractor },
powerInteractor = kosmos.powerInteractor,
alternateBouncerInteractor = kosmos.alternateBouncerInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
index 88a1df1..3388c75 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.notifications.ui.viewmodel
+import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -25,6 +26,7 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.ui.viewmodel.notificationsShadeOverlayContentViewModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
@@ -36,13 +38,14 @@
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@EnableSceneContainer
+@EnableFlags(DualShade.FLAG_NAME)
class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val sceneInteractor = kosmos.sceneInteractor
- private val underTest = kosmos.notificationsShadeOverlayContentViewModel
+ private val underTest by lazy { kosmos.notificationsShadeOverlayContentViewModel }
@Test
fun onScrimClicked_hidesShade() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
index abd1e2c..8c7ec47 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.qs.ui.viewmodel
+import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -25,6 +26,7 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -35,13 +37,14 @@
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@EnableSceneContainer
+@EnableFlags(DualShade.FLAG_NAME)
class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val sceneInteractor = kosmos.sceneInteractor
- private val underTest = kosmos.quickSettingsShadeOverlayContentViewModel
+ private val underTest by lazy { kosmos.quickSettingsShadeOverlayContentViewModel }
@Test
fun onScrimClicked_hidesShade() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index a0cafcb..c9e958d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -341,7 +341,7 @@
bouncerActionButton?.onClick?.invoke()
runCurrent()
- // TODO(b/298026988): Assert that an activity was started once we use ActivityStarter.
+ // TODO(b/369765704): Assert that an activity was started once we use ActivityStarter.
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt
new file mode 100644
index 0000000..efde1ec
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilterTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.ui.viewmodel
+
+import android.graphics.Region
+import android.view.setSystemGestureExclusionRegion
+import androidx.compose.ui.geometry.Offset
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.scene.sceneContainerGestureFilterFactory
+import com.android.systemui.settings.displayTracker
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SceneContainerGestureFilterTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val displayId = kosmos.displayTracker.defaultDisplayId
+
+ private val underTest = kosmos.sceneContainerGestureFilterFactory.create(displayId)
+ private val activationJob = Job()
+
+ @Test
+ fun shouldFilterGesture_whenNoRegion_returnsFalse() =
+ testScope.runTest {
+ activate()
+ setSystemGestureExclusionRegion(displayId, null)
+ runCurrent()
+
+ assertThat(underTest.shouldFilterGesture(Offset(100f, 100f))).isFalse()
+ }
+
+ @Test
+ fun shouldFilterGesture_whenOutsideRegion_returnsFalse() =
+ testScope.runTest {
+ activate()
+ setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+ runCurrent()
+
+ assertThat(underTest.shouldFilterGesture(Offset(300f, 100f))).isFalse()
+ }
+
+ @Test
+ fun shouldFilterGesture_whenInsideRegion_returnsTrue() =
+ testScope.runTest {
+ activate()
+ setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+ runCurrent()
+
+ assertThat(underTest.shouldFilterGesture(Offset(100f, 100f))).isTrue()
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun shouldFilterGesture_beforeActivation_throws() =
+ testScope.runTest {
+ setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+ runCurrent()
+
+ underTest.shouldFilterGesture(Offset(100f, 100f))
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun shouldFilterGesture_afterCancellation_throws() =
+ testScope.runTest {
+ activate()
+ setSystemGestureExclusionRegion(displayId, Region(0, 0, 200, 200))
+ runCurrent()
+
+ cancel()
+
+ underTest.shouldFilterGesture(Offset(100f, 100f))
+ }
+
+ private fun TestScope.activate() {
+ underTest.activateIn(testScope, activationJob)
+ runCurrent()
+ }
+
+ private fun TestScope.cancel() {
+ activationJob.cancel()
+ runCurrent()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index a0bb017..e60e742 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -36,10 +36,12 @@
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.fakeOverlaysByKeys
import com.android.systemui.scene.sceneContainerConfig
+import com.android.systemui.scene.sceneContainerGestureFilterFactory
import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.shared.flag.DualShade
@@ -86,6 +88,8 @@
shadeInteractor = kosmos.shadeInteractor,
splitEdgeDetector = kosmos.splitEdgeDetector,
logger = kosmos.sceneLogger,
+ gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
+ displayId = kosmos.displayTracker.defaultDisplayId,
motionEventHandlerReceiver = { motionEventHandler ->
this@SceneContainerViewModelTest.motionEventHandler = motionEventHandler
},
@@ -283,10 +287,7 @@
fakeSceneDataSource.showOverlay(Overlays.NotificationsShade)
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
assertThat(currentOverlays)
- .containsExactly(
- Overlays.QuickSettingsShade,
- Overlays.NotificationsShade,
- )
+ .containsExactly(Overlays.QuickSettingsShade, Overlays.NotificationsShade)
val actionableContentKey =
underTest.getActionableContentKey(
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 d163abf..19ac0cf 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
@@ -287,7 +287,7 @@
@Test
fun anyExpansion_shadeGreater() =
- testScope.runTest() {
+ testScope.runTest {
// WHEN shade is more expanded than QS
shadeTestUtil.setShadeAndQsExpansion(.5f, 0f)
runCurrent()
@@ -298,7 +298,7 @@
@Test
fun anyExpansion_qsGreater() =
- testScope.runTest() {
+ testScope.runTest {
// WHEN qs is more expanded than shade
shadeTestUtil.setShadeAndQsExpansion(0f, .5f)
runCurrent()
@@ -308,6 +308,36 @@
}
@Test
+ fun isShadeAnyExpanded_shadeCollapsed() =
+ testScope.runTest {
+ val isShadeAnyExpanded by collectLastValue(underTest.isShadeAnyExpanded)
+ shadeTestUtil.setShadeExpansion(0f)
+ runCurrent()
+
+ assertThat(isShadeAnyExpanded).isFalse()
+ }
+
+ @Test
+ fun isShadeAnyExpanded_shadePartiallyExpanded() =
+ testScope.runTest {
+ val isShadeAnyExpanded by collectLastValue(underTest.isShadeAnyExpanded)
+ shadeTestUtil.setShadeExpansion(0.01f)
+ runCurrent()
+
+ assertThat(isShadeAnyExpanded).isTrue()
+ }
+
+ @Test
+ fun isShadeAnyExpanded_shadeFullyExpanded() =
+ testScope.runTest {
+ val isShadeAnyExpanded by collectLastValue(underTest.isShadeAnyExpanded)
+ shadeTestUtil.setShadeExpansion(1f)
+ runCurrent()
+
+ assertThat(isShadeAnyExpanded).isTrue()
+ }
+
+ @Test
fun isShadeTouchable_isFalse_whenDeviceAsleepAndNotPulsing() =
testScope.runTest {
powerRepository.updateWakefulness(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
index 109cd05..4592b60 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
@@ -35,6 +35,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
@@ -382,4 +383,44 @@
// THEN user is not interacting
assertThat(actual).isFalse()
}
+
+ @Test
+ fun expandNotificationsShade_unsupported() =
+ testScope.runTest {
+ assertThrows(UnsupportedOperationException::class.java) {
+ underTest.expandNotificationsShade("reason")
+ }
+ }
+
+ @Test
+ fun expandQuickSettingsShade_unsupported() =
+ testScope.runTest {
+ assertThrows(UnsupportedOperationException::class.java) {
+ underTest.expandQuickSettingsShade("reason")
+ }
+ }
+
+ @Test
+ fun collapseNotificationsShade_unsupported() =
+ testScope.runTest {
+ assertThrows(UnsupportedOperationException::class.java) {
+ underTest.collapseNotificationsShade("reason")
+ }
+ }
+
+ @Test
+ fun collapseQuickSettingsShade_unsupported() =
+ testScope.runTest {
+ assertThrows(UnsupportedOperationException::class.java) {
+ underTest.collapseQuickSettingsShade("reason")
+ }
+ }
+
+ @Test
+ fun collapseEitherShade_unsupported() =
+ testScope.runTest {
+ assertThrows(UnsupportedOperationException::class.java) {
+ underTest.collapseEitherShade("reason")
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index f6fe667ff..eb8ea8b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -1,12 +1,15 @@
package com.android.systemui.shade.ui.viewmodel
import android.content.Intent
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.provider.AlarmClock
import android.provider.Settings
import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -18,7 +21,9 @@
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.plugins.activityStarter
import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor
import com.android.systemui.testKosmos
@@ -48,7 +53,7 @@
private val sceneInteractor = kosmos.sceneInteractor
private val deviceEntryInteractor = kosmos.deviceEntryInteractor
- private val underTest: ShadeHeaderViewModel = kosmos.shadeHeaderViewModel
+ private val underTest by lazy { kosmos.shadeHeaderViewModel }
@Before
fun setUp() {
@@ -96,6 +101,7 @@
}
@Test
+ @DisableFlags(DualShade.FLAG_NAME)
fun onSystemIconContainerClicked_locked_collapsesShadeToLockscreen() =
testScope.runTest {
setDeviceEntered(false)
@@ -108,6 +114,25 @@
}
@Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun onSystemIconContainerClicked_lockedOnDualShade_collapsesShadeToLockscreen() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ setDeviceEntered(false)
+ setScene(Scenes.Lockscreen)
+ setOverlay(Overlays.NotificationsShade)
+ assertThat(currentOverlays).isNotEmpty()
+
+ underTest.onSystemIconContainerClicked()
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(currentOverlays).isEmpty()
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
fun onSystemIconContainerClicked_unlocked_collapsesShadeToGone() =
testScope.runTest {
setDeviceEntered(true)
@@ -119,6 +144,24 @@
assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
}
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun onSystemIconContainerClicked_unlockedOnDualShade_collapsesShadeToGone() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ setDeviceEntered(true)
+ setScene(Scenes.Gone)
+ setOverlay(Overlays.NotificationsShade)
+ assertThat(currentOverlays).isNotEmpty()
+
+ underTest.onSystemIconContainerClicked()
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ assertThat(currentOverlays).isEmpty()
+ }
+
companion object {
private val SUB_1 =
SubscriptionModel(
@@ -144,6 +187,17 @@
testScope.runCurrent()
}
+ private fun setOverlay(key: OverlayKey) {
+ val currentOverlays = sceneInteractor.currentOverlays.value + key
+ sceneInteractor.showOverlay(key, "test")
+ sceneInteractor.setTransitionState(
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(sceneInteractor.currentScene.value, currentOverlays)
+ )
+ )
+ testScope.runCurrent()
+ }
+
private fun TestScope.setDeviceEntered(isEntered: Boolean) {
if (isEntered) {
// Unlock the device marking the device has entered.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
index 7b87aeb..d772e3e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -42,7 +42,8 @@
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.domain.interactor.lockScreenNotificationMinimalismSetting
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.testKosmos
import com.android.systemui.util.settings.FakeSettings
@@ -66,7 +67,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-@EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+@EnableFlags(NotificationMinimalism.FLAG_NAME)
class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
private val kosmos =
@@ -76,7 +77,7 @@
mock<SysuiStatusBarStateController>().also { mock ->
doAnswer { statusBarState.ordinal }.whenever(mock).state
}
- fakeSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS, 1)
+ lockScreenNotificationMinimalismSetting = true
}
private val notifPipeline: NotifPipeline = mock()
private var statusBarState: StatusBarState = StatusBarState.KEYGUARD
@@ -193,7 +194,7 @@
kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
assertThat(promoter.shouldPromoteToTopLevel(child1)).isFalse()
assertThat(promoter.shouldPromoteToTopLevel(child2))
- .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+ .isEqualTo(NotificationMinimalism.ungroupTopUnseen)
assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
@@ -201,7 +202,7 @@
kosmos.activeNotificationListRepository.topUnseenNotificationKey.value = child2.key
assertThat(promoter.shouldPromoteToTopLevel(child1)).isTrue()
assertThat(promoter.shouldPromoteToTopLevel(child2))
- .isEqualTo(NotificationMinimalismPrototype.ungroupTopUnseen)
+ .isEqualTo(NotificationMinimalism.ungroupTopUnseen)
assertThat(promoter.shouldPromoteToTopLevel(child3)).isFalse()
assertThat(promoter.shouldPromoteToTopLevel(parent)).isFalse()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
index 2159b86..ea2e25e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorTest.kt
@@ -21,7 +21,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -57,7 +57,7 @@
}
@Test
- @EnableFlags(NotificationMinimalismPrototype.FLAG_NAME)
+ @EnableFlags(NotificationMinimalism.FLAG_NAME)
fun topOngoingAndUnseenNotification() = runTest {
val entry1 = NotificationEntryBuilder().setTag("entry1").build()
val entry2 = NotificationEntryBuilder().setTag("entry2").build()
@@ -91,4 +91,17 @@
testScheduler.runCurrent()
assertThat(settingEnabled).isTrue()
}
+
+ fun testLockScreenNotificationMinimalismSetting() = runTest {
+ val settingEnabled by
+ collectLastValue(underTest.isLockScreenNotificationMinimalismEnabled())
+
+ kosmos.lockScreenNotificationMinimalismSetting = false
+ testScheduler.runCurrent()
+ assertThat(settingEnabled).isFalse()
+
+ kosmos.lockScreenNotificationMinimalismSetting = true
+ testScheduler.runCurrent()
+ assertThat(settingEnabled).isTrue()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
new file mode 100644
index 0000000..f9b7769
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelTest.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.emptyshade.ui.viewmodel
+
+import android.app.Flags
+import android.app.NotificationManager.Policy
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
+import android.provider.Settings
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
+import androidx.test.filters.SmallTest
+import com.android.settingslib.notification.data.repository.updateNotificationPolicy
+import com.android.settingslib.notification.modes.TestModeBuilder
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.andSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
+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.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(ParameterizedAndroidJunit4::class)
+@SmallTest
+@EnableFlags(FooterViewRefactor.FLAG_NAME)
+class EmptyShadeViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val zenModeRepository = kosmos.zenModeRepository
+ private val activeNotificationListRepository = kosmos.activeNotificationListRepository
+
+ private val underTest = kosmos.emptyShadeViewModel
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf().andSceneContainer()
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ @Test
+ fun areNotificationsHiddenInShade_true() =
+ testScope.runTest {
+ val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+ runCurrent()
+
+ assertThat(hidden).isTrue()
+ }
+
+ @Test
+ fun areNotificationsHiddenInShade_false() =
+ testScope.runTest {
+ val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+ runCurrent()
+
+ assertThat(hidden).isFalse()
+ }
+
+ @Test
+ fun hasFilteredOutSeenNotifications_true() =
+ testScope.runTest {
+ val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
+ runCurrent()
+
+ assertThat(hasFilteredNotifs).isTrue()
+ }
+
+ @Test
+ fun hasFilteredOutSeenNotifications_false() =
+ testScope.runTest {
+ val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+ runCurrent()
+
+ assertThat(hasFilteredNotifs).isFalse()
+ }
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+ @DisableFlags(Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ fun text_changesWhenNotifsHiddenInShade() =
+ testScope.runTest {
+ val text by collectLastValue(underTest.text)
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
+ runCurrent()
+
+ assertThat(text).isEqualTo("No notifications")
+
+ zenModeRepository.updateNotificationPolicy(
+ suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
+ )
+ zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+ runCurrent()
+
+ assertThat(text).isEqualTo("Notifications paused by Do Not Disturb")
+ }
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ fun text_reflectsModesHidingNotifications() =
+ testScope.runTest {
+ val text by collectLastValue(underTest.text)
+
+ assertThat(text).isEqualTo("No notifications")
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setId("Do not disturb")
+ .setName("Do not disturb")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build()
+ )
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Do not disturb")
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setId("Work")
+ .setName("Work")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build()
+ )
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Do not disturb and one other mode")
+
+ zenModeRepository.addMode(
+ TestModeBuilder()
+ .setId("Gym")
+ .setName("Gym")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build()
+ )
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Do not disturb and 2 other modes")
+
+ zenModeRepository.deactivateMode("Do not disturb")
+ zenModeRepository.deactivateMode("Work")
+ runCurrent()
+ assertThat(text).isEqualTo("Notifications paused by Gym")
+ }
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME)
+ fun footer_isVisibleWhenSeenNotifsAreFilteredOut() =
+ testScope.runTest {
+ val footerVisible by collectLastValue(underTest.footer.isVisible)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+ runCurrent()
+
+ assertThat(footerVisible).isFalse()
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
+ runCurrent()
+
+ assertThat(footerVisible).isTrue()
+ }
+}
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 26e1a4d..d12d6f6 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
@@ -18,12 +18,9 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
-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.filters.SmallTest
-import com.android.settingslib.notification.data.repository.updateNotificationPolicy
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.DisableSceneContainer
@@ -46,7 +43,6 @@
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository
-import com.android.systemui.statusbar.policy.data.repository.zenModeRepository
import com.android.systemui.statusbar.policy.fakeConfigurationController
import com.android.systemui.testKosmos
import com.android.systemui.util.ui.isAnimating
@@ -79,7 +75,6 @@
private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository
private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository
private val headsUpRepository = kosmos.headsUpNotificationRepository
- private val zenModeRepository = kosmos.zenModeRepository
private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
@@ -266,56 +261,6 @@
}
@Test
- fun areNotificationsHiddenInShade_true() =
- testScope.runTest {
- val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
- zenModeRepository.updateNotificationPolicy(
- suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
- )
- zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
- runCurrent()
-
- assertThat(hidden).isTrue()
- }
-
- @Test
- fun areNotificationsHiddenInShade_false() =
- testScope.runTest {
- val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
-
- zenModeRepository.updateNotificationPolicy(
- suppressedVisualEffects = Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST
- )
- zenModeRepository.updateZenMode(Settings.Global.ZEN_MODE_OFF)
- runCurrent()
-
- assertThat(hidden).isFalse()
- }
-
- @Test
- fun hasFilteredOutSeenNotifications_true() =
- testScope.runTest {
- val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
-
- activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
- runCurrent()
-
- assertThat(hasFilteredNotifs).isTrue()
- }
-
- @Test
- fun hasFilteredOutSeenNotifications_false() =
- testScope.runTest {
- val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
-
- activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
- runCurrent()
-
- assertThat(hasFilteredNotifs).isFalse()
- }
-
- @Test
fun shouldIncludeFooterView_trueWhenShade() =
testScope.runTest {
val shouldIncludeFooterView by collectFooterViewVisibility()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
index 2c8cc1a..3053672 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/HeadsUpManagerPhoneTest.kt
@@ -47,7 +47,6 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.settings.GlobalSettings
import com.android.systemui.util.time.SystemClock
-import com.google.common.truth.Truth.assertThat
import junit.framework.Assert
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -248,35 +247,32 @@
@Test
@EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testShowNotification_removeWhenReorderingAllowedTrue() {
- whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
- val hmp = createHeadsUpManagerPhone()
-
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
- hmp.showNotification(notifEntry)
- assertThat(hmp.mEntriesToRemoveWhenReorderingAllowed.contains(notifEntry)).isTrue();
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testShowNotification_reorderNotAllowed_seenInShadeTrue() {
+ fun testShowNotification_reorderNotAllowed_notPulsing_seenInShadeTrue() {
whenever(mVSProvider.isReorderingAllowed).thenReturn(false)
val hmp = createHeadsUpManagerPhone()
val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val row = mock<ExpandableNotificationRow>()
+ whenever(row.showingPulsing()).thenReturn(false)
+ notifEntry.row = row
+
hmp.showNotification(notifEntry)
- assertThat(notifEntry.isSeenInShade).isTrue();
+ Assert.assertTrue(notifEntry.isSeenInShade)
}
@Test
@EnableFlags(NotificationThrottleHun.FLAG_NAME)
- fun testShowNotification_reorderAllowed_seenInShadeFalse() {
+ fun testShowNotification_reorderAllowed_notPulsing_seenInShadeFalse() {
whenever(mVSProvider.isReorderingAllowed).thenReturn(true)
val hmp = createHeadsUpManagerPhone()
val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val row = mock<ExpandableNotificationRow>()
+ whenever(row.showingPulsing()).thenReturn(false)
+ notifEntry.row = row
+
hmp.showNotification(notifEntry)
- assertThat(notifEntry.isSeenInShade).isFalse();
+ Assert.assertFalse(notifEntry.isSeenInShade)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 0f6dc07..c5ccf9e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -25,6 +25,7 @@
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
import android.service.notification.SystemZenRules
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.R
@@ -34,6 +35,7 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository
import com.android.systemui.testKosmos
@@ -379,4 +381,46 @@
assertThat(dndMode!!.isActive).isTrue()
}
+
+ @Test
+ @EnableFlags(ModesEmptyShadeFix.FLAG_NAME, Flags.FLAG_MODES_UI, Flags.FLAG_MODES_API)
+ fun modesHidingNotifications_onlyIncludesModesWithNotifListSuppression() =
+ testScope.runTest {
+ val modesHidingNotifications by collectLastValue(underTest.modesHidingNotifications)
+
+ zenModeRepository.addModes(
+ listOf(
+ TestModeBuilder()
+ .setName("Not active, no list suppression")
+ .setActive(false)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ true)
+ .build(),
+ TestModeBuilder()
+ .setName("Not active, has list suppression")
+ .setActive(false)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build(),
+ TestModeBuilder()
+ .setName("No list suppression")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ true)
+ .build(),
+ TestModeBuilder()
+ .setName("Has list suppression 1")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build(),
+ TestModeBuilder()
+ .setName("Has list suppression 2")
+ .setActive(true)
+ .setVisualEffect(VISUAL_EFFECT_NOTIFICATION_LIST, /* allowed= */ false)
+ .build(),
+ )
+ )
+ runCurrent()
+
+ assertThat(modesHidingNotifications?.map { it.name })
+ .containsExactly("Has list suppression 1", "Has list suppression 2")
+ .inOrder()
+ }
}
diff --git a/packages/SystemUI/res/layout/status_bar_no_notifications.xml b/packages/SystemUI/res/layout/status_bar_no_notifications.xml
index 248e611..e26b855 100644
--- a/packages/SystemUI/res/layout/status_bar_no_notifications.xml
+++ b/packages/SystemUI/res/layout/status_bar_no_notifications.xml
@@ -26,6 +26,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
+ android:paddingHorizontal="8dp"
>
<TextView
android:id="@+id/no_notifications"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 24b6579..75389b1 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1473,6 +1473,16 @@
<!-- The text to show in the notifications shade when dnd is suppressing notifications. [CHAR LIMIT=100] -->
<string name="dnd_suppressing_shade_text">Notifications paused by Do Not Disturb</string>
+ <!-- The text to show in the notifications shade when a mode is suppressing notifications. [CHAR LIMIT=100] -->
+ <string name="modes_suppressing_shade_text">
+ {count, plural, offset:1
+ =0 {No notifications}
+ =1 {Notifications paused by {mode}}
+ =2 {Notifications paused by {mode} and one other mode}
+ other {Notifications paused by {mode} and # other modes}
+ }
+ </string>
+
<!-- Media projection permission dialog action text. [CHAR LIMIT=60] -->
<string name="media_projection_action_text">Start now</string>
diff --git a/packages/SystemUI/shared/Android.bp b/packages/SystemUI/shared/Android.bp
index 8f55961..0f1da50 100644
--- a/packages/SystemUI/shared/Android.bp
+++ b/packages/SystemUI/shared/Android.bp
@@ -70,6 +70,7 @@
"jsr330",
"//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
"//frameworks/libs/systemui:msdl",
+ "//frameworks/libs/systemui:view_capture",
],
resource_dirs: [
"res",
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
index f358ba2..4db6ab6 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
@@ -16,6 +16,9 @@
package com.android.systemui.shared.rotation;
+import static com.android.app.viewcapture.ViewCaptureFactory.getViewCaptureAwareWindowManagerInstance;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+
import android.annotation.DimenRes;
import android.annotation.IdRes;
import android.annotation.LayoutRes;
@@ -30,7 +33,6 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
@@ -38,6 +40,7 @@
import androidx.annotation.BoolRes;
import androidx.core.view.OneShotPreDrawListener;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position;
/**
@@ -47,7 +50,7 @@
private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300;
- private final WindowManager mWindowManager;
+ private final ViewCaptureAwareWindowManager mWindowManager;
private final ViewGroup mKeyButtonContainer;
private final FloatingRotationButtonView mKeyButtonView;
@@ -88,7 +91,8 @@
@DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter,
@DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) {
mContext = context;
- mWindowManager = mContext.getSystemService(WindowManager.class);
+ mWindowManager = getViewCaptureAwareWindowManagerInstance(mContext,
+ enableViewCaptureTracing());
mKeyButtonContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(layout, null);
mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId);
mKeyButtonView.setVisibility(View.VISIBLE);
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
index 8b5a09b..2c026c0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt
@@ -103,7 +103,7 @@
prepareToPerformAction()
returnToCall()
},
- onLongClick = null
+ onLongClick = null,
)
}
@@ -115,15 +115,15 @@
dozeLogger.logEmergencyCall()
startEmergencyDialerActivity()
},
- // TODO(b/308001302): The long click detector doesn't work properly, investigate.
+ // TODO(b/369767936): The long click detector doesn't work properly, investigate.
onLongClick = {
if (emergencyAffordanceManager.needsEmergencyAffordance()) {
prepareToPerformAction()
- // TODO(b/298026988): Check that !longPressWasDragged before invoking.
+ // TODO(b/369767936): Check that !longPressWasDragged before invoking.
emergencyAffordanceManager.performEmergencyCall()
}
- }
+ },
)
}
@@ -143,7 +143,7 @@
applicationContext.startActivityAsUser(
this,
ActivityOptions.makeCustomAnimation(applicationContext, 0, 0).toBundle(),
- UserHandle(selectedUserInteractor.getSelectedUserId())
+ UserHandle(selectedUserInteractor.getSelectedUserId()),
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 0b775ab..3cc184d 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -35,9 +35,11 @@
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import javax.inject.Inject
@@ -55,7 +57,9 @@
// Internal notification frontend dependencies
NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
- NotificationMinimalismPrototype.token dependsOn NotificationThrottleHun.token
+ NotificationMinimalism.token dependsOn NotificationThrottleHun.token
+ ModesEmptyShadeFix.token dependsOn FooterViewRefactor.token
+ ModesEmptyShadeFix.token dependsOn modesUi
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 493afde..aa1873c 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -2298,9 +2298,11 @@
}
@Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ public boolean onScroll(@Nullable MotionEvent e1, MotionEvent e2,
+ float distanceX,
float distanceY) {
if (distanceY < 0 && distanceY > distanceX
+ && e1 != null
&& e1.getY() <= mStatusBarWindowController.getStatusBarHeight()) {
// Downwards scroll from top
openShadeAndDismiss();
@@ -2310,9 +2312,11 @@
}
@Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ public boolean onFling(@Nullable MotionEvent e1, MotionEvent e2,
+ float velocityX,
float velocityY) {
if (velocityY > 0 && Math.abs(velocityY) > Math.abs(velocityX)
+ && e1 != null
&& e1.getY() <= mStatusBarWindowController.getStatusBarHeight()) {
// Downwards fling from top
openShadeAndDismiss();
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
index 68a252b..654c763 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -515,7 +515,13 @@
}
protected void notifyChange() {
- mBgHandler.post(() -> mContentResolver.notifyChange(mSliceUri, null /* observer */));
+ mBgHandler.post(() -> {
+ try {
+ mContentResolver.notifyChange(mSliceUri, null /* observer */);
+ } catch (Exception e) {
+ Log.e(TAG, "Error on mContentResolver.notifyChange()", e);
+ }
+ });
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 416eaba..063adc8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -63,6 +63,7 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.KeyguardIndicationController
import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLockscreenScrimViewModel
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
@@ -102,6 +103,7 @@
private val keyguardClockViewModel: KeyguardClockViewModel,
private val smartspaceViewModel: KeyguardSmartspaceViewModel,
private val lockscreenContentViewModelFactory: LockscreenContentViewModel.Factory,
+ private val notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>,
private val clockInteractor: KeyguardClockInteractor,
private val keyguardViewMediator: KeyguardViewMediator,
@@ -207,6 +209,7 @@
private fun createLockscreen(
context: Context,
viewModelFactory: LockscreenContentViewModel.Factory,
+ notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory,
blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>,
): View {
val sceneBlueprints =
@@ -222,6 +225,8 @@
with(
LockscreenContent(
viewModelFactory = viewModelFactory,
+ notificationScrimViewModelFactory =
+ notificationScrimViewModelFactory,
blueprints = sceneBlueprints,
clockInteractor = clockInteractor,
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
index ea80911..18b1495 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractor.kt
@@ -28,7 +28,6 @@
import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -60,7 +59,6 @@
transitionInteractor: KeyguardTransitionInteractor,
val dismissInteractor: KeyguardDismissInteractor,
@Application private val applicationScope: CoroutineScope,
- sceneInteractor: Lazy<SceneInteractor>,
deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>,
powerInteractor: PowerInteractor,
alternateBouncerInteractor: AlternateBouncerInteractor,
@@ -102,20 +100,20 @@
private val isOnShadeWhileUnlocked: Flow<Boolean> =
if (SceneContainerFlag.isEnabled) {
combine(
- sceneInteractor.get().currentScene,
+ shadeInteractor.get().isAnyExpanded,
deviceUnlockedInteractor.get().deviceUnlockStatus,
- ) { scene, unlockStatus ->
- unlockStatus.isUnlocked &&
- (scene == Scenes.QuickSettings || scene == Scenes.Shade)
+ ) { isAnyExpanded, unlockStatus ->
+ isAnyExpanded && unlockStatus.isUnlocked
}
.distinctUntilChanged()
} else if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) {
combine(
- shadeInteractor.get().isAnyExpanded,
- keyguardInteractor.get().isKeyguardDismissible,
- ) { isAnyExpanded, keyguardDismissible ->
- isAnyExpanded && keyguardDismissible
- }
+ shadeInteractor.get().isAnyExpanded,
+ keyguardInteractor.get().isKeyguardDismissible,
+ ) { isAnyExpanded, keyguardDismissible ->
+ isAnyExpanded && keyguardDismissible
+ }
+ .distinctUntilChanged()
} else {
flow {
error(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
index 275f1ee..39cedc3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListener.kt
@@ -128,7 +128,7 @@
data: MediaData,
immediately: Boolean,
receivedSmartspaceCardLatency: Int,
- isSsReactivated: Boolean
+ isSsReactivated: Boolean,
) {
var reusedListener: PlaybackStateListener? = null
@@ -183,7 +183,7 @@
override fun onSmartspaceMediaDataLoaded(
key: String,
data: SmartspaceMediaData,
- shouldPrioritize: Boolean
+ shouldPrioritize: Boolean,
) {
if (!mediaFlags.isPersistentSsCardEnabled()) return
@@ -259,7 +259,9 @@
}
override fun onPlaybackStateChanged(state: PlaybackState?) {
- processState(state, dispatchEvents = true, currentResumption = resumption)
+ bgExecutor.execute {
+ processState(state, dispatchEvents = true, currentResumption = resumption)
+ }
}
override fun onSessionDestroyed() {
@@ -276,6 +278,7 @@
}
}
+ @WorkerThread
private fun processState(
state: PlaybackState?,
dispatchEvents: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
index 5be225c..219e45c 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
@@ -16,8 +16,7 @@
package com.android.systemui.notifications.ui.viewmodel
-import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import dagger.assisted.AssistedFactory
@@ -34,13 +33,10 @@
constructor(
val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
- private val sceneInteractor: SceneInteractor,
+ private val shadeInteractor: ShadeInteractor,
) {
fun onScrimClicked() {
- sceneInteractor.hideOverlay(
- overlay = Overlays.NotificationsShade,
- loggingReason = "Shade scrim clicked",
- )
+ shadeInteractor.collapseNotificationsShade(loggingReason = "Shade scrim clicked")
}
@AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index c807960..ba0d938 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -17,12 +17,15 @@
package com.android.systemui.qs.composefragment
import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PointF
import android.graphics.Rect
import android.os.Bundle
import android.util.IndentingPrintWriter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.FrameLayout
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
@@ -185,62 +188,76 @@
savedInstanceState: Bundle?,
): View {
val context = inflater.context
- return ComposeView(context).apply {
- setBackPressedDispatcher()
- setContent {
- PlatformTheme {
- val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
+ val composeView =
+ ComposeView(context).apply {
+ setBackPressedDispatcher()
+ setContent {
+ PlatformTheme {
+ val visible by viewModel.qsVisible.collectAsStateWithLifecycle()
- AnimatedVisibility(
- visible = visible,
- modifier =
- Modifier.windowInsetsPadding(WindowInsets.navigationBars)
- .thenIf(notificationScrimClippingParams.isEnabled) {
- Modifier.notificationScrimClip(
- notificationScrimClippingParams.leftInset,
- notificationScrimClippingParams.top,
- notificationScrimClippingParams.rightInset,
- notificationScrimClippingParams.bottom,
- notificationScrimClippingParams.radius,
+ AnimatedVisibility(
+ visible = visible,
+ modifier =
+ Modifier.windowInsetsPadding(WindowInsets.navigationBars)
+ .thenIf(notificationScrimClippingParams.isEnabled) {
+ Modifier.notificationScrimClip(
+ notificationScrimClippingParams.leftInset,
+ notificationScrimClippingParams.top,
+ notificationScrimClippingParams.rightInset,
+ notificationScrimClippingParams.bottom,
+ notificationScrimClippingParams.radius,
+ )
+ }
+ .graphicsLayer { elevation = 4.dp.toPx() },
+ ) {
+ val isEditing by
+ viewModel.containerViewModel.editModeViewModel.isEditing
+ .collectAsStateWithLifecycle()
+ val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS)
+ AnimatedContent(
+ targetState = isEditing,
+ transitionSpec = {
+ fadeIn(animationSpecEditMode) togetherWith
+ fadeOut(animationSpecEditMode)
+ },
+ label = "EditModeAnimatedContent",
+ ) { editing ->
+ if (editing) {
+ val qqsPadding by
+ viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
+ EditMode(
+ viewModel = viewModel.containerViewModel.editModeViewModel,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(top = { qqsPadding })
+ .padding(
+ horizontal = {
+ QuickSettingsShade.Dimensions.Padding
+ .roundToPx()
+ }
+ ),
)
+ } else {
+ CollapsableQuickSettingsSTL()
}
- .graphicsLayer { elevation = 4.dp.toPx() },
- ) {
- val isEditing by
- viewModel.containerViewModel.editModeViewModel.isEditing
- .collectAsStateWithLifecycle()
- val animationSpecEditMode = tween<Float>(EDIT_MODE_TIME_MILLIS)
- AnimatedContent(
- targetState = isEditing,
- transitionSpec = {
- fadeIn(animationSpecEditMode) togetherWith
- fadeOut(animationSpecEditMode)
- },
- label = "EditModeAnimatedContent",
- ) { editing ->
- if (editing) {
- val qqsPadding by
- viewModel.qqsHeaderHeight.collectAsStateWithLifecycle()
- EditMode(
- viewModel = viewModel.containerViewModel.editModeViewModel,
- modifier =
- Modifier.fillMaxWidth()
- .padding(top = { qqsPadding })
- .padding(
- horizontal = {
- QuickSettingsShade.Dimensions.Padding
- .roundToPx()
- }
- ),
- )
- } else {
- CollapsableQuickSettingsSTL()
}
}
}
}
}
- }
+
+ val frame =
+ FrameLayoutTouchPassthrough(
+ context,
+ { notificationScrimClippingParams.isEnabled },
+ { notificationScrimClippingParams.top },
+ )
+ frame.addView(
+ composeView,
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ )
+ return frame
}
/**
@@ -762,3 +779,26 @@
}
private const val EDIT_MODE_TIME_MILLIS = 500
+
+/**
+ * Ignore touches below the value returned by [clippingTopProvider], when clipping is enabled, as
+ * per [clippingEnabledProvider].
+ */
+private class FrameLayoutTouchPassthrough(
+ context: Context,
+ private val clippingEnabledProvider: () -> Boolean,
+ private val clippingTopProvider: () -> Int,
+) : FrameLayout(context) {
+ override fun isTransformedTouchPointInView(
+ x: Float,
+ y: Float,
+ child: View?,
+ outLocalPoint: PointF?,
+ ): Boolean {
+ return if (clippingEnabledProvider() && y + translationY > clippingTopProvider()) {
+ false
+ } else {
+ super.isTransformedTouchPointInView(x, y, child, outLocalPoint)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
index 7c62e59..e0f0b6a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -20,14 +20,18 @@
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.systemGestureExclusion
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.toSize
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingDotSize
/**
@@ -52,14 +56,18 @@
// not receive the touch input accidentally.
val minTouchTargetSize = LocalMinimumInteractiveComponentSize.current
Box(
- Modifier.size(minTouchTargetSize).pointerInput(Unit) {
- detectHorizontalDragGestures(
- onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) },
- onDragStart = { tileWidths()?.let { selectionState.onResizingDragStart(it) } },
- onDragEnd = selectionState::onResizingDragEnd,
- onDragCancel = selectionState::onResizingDragEnd,
- )
- }
+ Modifier.size(minTouchTargetSize)
+ .systemGestureExclusion { Rect(Offset.Zero, it.size.toSize()) }
+ .pointerInput(Unit) {
+ detectHorizontalDragGestures(
+ onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) },
+ onDragStart = {
+ tileWidths()?.let { selectionState.onResizingDragStart(it) }
+ },
+ onDragEnd = selectionState::onResizingDragEnd,
+ onDragCancel = selectionState::onResizingDragEnd,
+ )
+ }
) {
ResizingDot(transition = transition, modifier = Modifier.align(Alignment.Center))
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
index 6a8cc17..4f3ea83 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
@@ -126,12 +126,12 @@
private val overlayColorActive =
Utils.applyAlpha(
/* alpha= */ 0.11f,
- Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
+ Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive),
)
private val overlayColorInactive =
Utils.applyAlpha(
/* alpha= */ 0.08f,
- Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive)
+ Utils.getColorAttrDefaultColor(context, R.attr.onShadeInactive),
)
private val colorLabelActive = Utils.getColorAttrDefaultColor(context, R.attr.onShadeActive)
@@ -188,10 +188,7 @@
private var lastState = INVALID
private var lastIconTint = 0
private val launchableViewDelegate =
- LaunchableViewDelegate(
- this,
- superSetVisibility = { super.setVisibility(it) },
- )
+ LaunchableViewDelegate(this, superSetVisibility = { super.setVisibility(it) })
private var lastDisabledByPolicy = false
private val locInScreen = IntArray(2)
@@ -418,7 +415,7 @@
initLongPressEffectCallback()
init(
{ _: View -> longPressEffect.onTileClick() },
- null, // Haptics and long-clicks will be handled by the [QSLongPressEffect]
+ { _: View -> true }, // Haptics and long-clicks are handled by [QSLongPressEffect]
)
} else {
val expandable = Expandable.fromView(this)
@@ -583,7 +580,7 @@
AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id,
resources.getString(
R.string.accessibility_tile_disabled_by_policy_action_description
- )
+ ),
)
)
} else {
@@ -591,7 +588,7 @@
info.addAction(
AccessibilityNodeInfo.AccessibilityAction(
AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.id,
- resources.getString(R.string.accessibility_long_click_tile)
+ resources.getString(R.string.accessibility_long_click_tile),
)
)
}
@@ -716,35 +713,35 @@
state.spec,
state.state,
state.disabledByPolicy,
- getBackgroundColorForState(state.state, state.disabledByPolicy)
+ getBackgroundColorForState(state.state, state.disabledByPolicy),
)
if (allowAnimations) {
singleAnimator.setValues(
colorValuesHolder(
BACKGROUND_NAME,
backgroundColor,
- getBackgroundColorForState(state.state, state.disabledByPolicy)
+ getBackgroundColorForState(state.state, state.disabledByPolicy),
),
colorValuesHolder(
LABEL_NAME,
label.currentTextColor,
- getLabelColorForState(state.state, state.disabledByPolicy)
+ getLabelColorForState(state.state, state.disabledByPolicy),
),
colorValuesHolder(
SECONDARY_LABEL_NAME,
secondaryLabel.currentTextColor,
- getSecondaryLabelColorForState(state.state, state.disabledByPolicy)
+ getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
),
colorValuesHolder(
CHEVRON_NAME,
chevronView.imageTintList?.defaultColor ?: 0,
- getChevronColorForState(state.state, state.disabledByPolicy)
+ getChevronColorForState(state.state, state.disabledByPolicy),
),
colorValuesHolder(
OVERLAY_NAME,
backgroundOverlayColor,
- getOverlayColorForState(state.state)
- )
+ getOverlayColorForState(state.state),
+ ),
)
singleAnimator.start()
} else {
@@ -753,7 +750,7 @@
getLabelColorForState(state.state, state.disabledByPolicy),
getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
getChevronColorForState(state.state, state.disabledByPolicy),
- getOverlayColorForState(state.state)
+ getOverlayColorForState(state.state),
)
}
}
@@ -1077,7 +1074,7 @@
backgroundColor,
label.currentTextColor,
secondaryLabel.currentTextColor,
- chevronView.imageTintList?.defaultColor ?: 0
+ chevronView.imageTintList?.defaultColor ?: 0,
)
inner class StateChangeRunnable(private val state: QSTile.State) : Runnable {
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 5ea9e6a..301ab2b 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
@@ -311,10 +311,7 @@
mConfig = MobileMappings.Config.readConfig(mContext);
mTelephonyManager = mTelephonyManager.createForSubscriptionId(mDefaultDataSubId);
mSubIdTelephonyManagerMap.put(mDefaultDataSubId, mTelephonyManager);
- InternetTelephonyCallback telephonyCallback =
- new InternetTelephonyCallback(mDefaultDataSubId);
- mSubIdTelephonyCallbackMap.put(mDefaultDataSubId, telephonyCallback);
- mTelephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback);
+ registerInternetTelephonyCallback(mTelephonyManager, mDefaultDataSubId);
// Listen the connectivity changes
mConnectivityManager.registerDefaultNetworkCallback(mConnectivityManagerNetworkCallback);
mCanConfigWifi = canConfigWifi;
@@ -346,6 +343,23 @@
mCallback = null;
}
+ /**
+ * This is to generate and register the new callback to Telephony for uncached subscription id,
+ * then cache it. Telephony also cached this callback into
+ * {@link com.android.server.TelephonyRegistry}, so if subscription id and callback were cached
+ * already, it shall do nothing to avoid registering redundant callback to Telephony.
+ */
+ private void registerInternetTelephonyCallback(
+ TelephonyManager telephonyManager, int subId) {
+ if (mSubIdTelephonyCallbackMap.containsKey(subId)) {
+ // Avoid to generate and register unnecessary callback to Telephony.
+ return;
+ }
+ InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId);
+ mSubIdTelephonyCallbackMap.put(subId, telephonyCallback);
+ telephonyManager.registerTelephonyCallback(mExecutor, telephonyCallback);
+ }
+
boolean isAirplaneModeEnabled() {
return mGlobalSettings.getInt(Settings.Global.AIRPLANE_MODE_ON, 0) != 0;
}
@@ -673,9 +687,7 @@
int subId = subInfo.getSubscriptionId();
if (mSubIdTelephonyManagerMap.get(subId) == null) {
TelephonyManager secondaryTm = mTelephonyManager.createForSubscriptionId(subId);
- InternetTelephonyCallback telephonyCallback = new InternetTelephonyCallback(subId);
- secondaryTm.registerTelephonyCallback(mExecutor, telephonyCallback);
- mSubIdTelephonyCallbackMap.put(subId, telephonyCallback);
+ registerInternetTelephonyCallback(secondaryTm, subId);
mSubIdTelephonyManagerMap.put(subId, secondaryTm);
}
return subId;
@@ -1351,6 +1363,7 @@
if (DEBUG) {
Log.d(TAG, "DDS: defaultDataSubId:" + defaultDataSubId);
}
+
if (SubscriptionManager.isUsableSubscriptionId(defaultDataSubId)) {
// clean up old defaultDataSubId
TelephonyCallback oldCallback = mSubIdTelephonyCallbackMap.get(mDefaultDataSubId);
@@ -1366,9 +1379,7 @@
// create for new defaultDataSubId
mTelephonyManager = mTelephonyManager.createForSubscriptionId(defaultDataSubId);
mSubIdTelephonyManagerMap.put(defaultDataSubId, mTelephonyManager);
- InternetTelephonyCallback newCallback = new InternetTelephonyCallback(defaultDataSubId);
- mSubIdTelephonyCallbackMap.put(defaultDataSubId, newCallback);
- mTelephonyManager.registerTelephonyCallback(mHandler::post, newCallback);
+ registerInternetTelephonyCallback(mTelephonyManager, defaultDataSubId);
mCallback.onSubscriptionsChanged(defaultDataSubId);
}
mDefaultDataSubId = defaultDataSubId;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
index 3b97d82..7c8fbea 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
@@ -16,8 +16,7 @@
package com.android.systemui.qs.ui.viewmodel
-import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -31,15 +30,12 @@
class QuickSettingsShadeOverlayContentViewModel
@AssistedInject
constructor(
- val sceneInteractor: SceneInteractor,
+ val shadeInteractor: ShadeInteractor,
val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
val quickSettingsContainerViewModel: QuickSettingsContainerViewModel,
) {
fun onScrimClicked() {
- sceneInteractor.hideOverlay(
- overlay = Overlays.QuickSettingsShade,
- loggingReason = "Shade scrim clicked",
- )
+ shadeInteractor.collapseQuickSettingsShade(loggingReason = "Shade scrim clicked")
}
@AssistedFactory
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index ac49e91..559c263 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -248,7 +248,8 @@
} else if (action == ACTION_UP) {
// Gesture was too short to be picked up by scene container touch
// handling; programmatically start the transition to the shade.
- mShadeInteractor.get().expandNotificationShade("short launcher swipe");
+ mShadeInteractor.get()
+ .expandNotificationsShade("short launcher swipe", null);
}
}
event.recycle();
@@ -265,7 +266,8 @@
mSceneInteractor.get().onRemoteUserInputStarted(
"trackpad swipe");
} else if (action == ACTION_UP) {
- mShadeInteractor.get().expandNotificationShade("short trackpad swipe");
+ mShadeInteractor.get()
+ .expandNotificationsShade("short trackpad swipe", null);
}
mStatusBarWinController.getWindowRootView().dispatchTouchEvent(event);
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt
new file mode 100644
index 0000000..a8d0777
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepository.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.data.repository
+
+import android.graphics.Region
+import android.view.ISystemGestureExclusionListener
+import android.view.IWindowManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class SystemGestureExclusionRepository
+@Inject
+constructor(private val windowManager: IWindowManager) {
+
+ /**
+ * Returns [Flow] of the [Region] in which system gestures should be excluded on the display
+ * identified with [displayId].
+ */
+ fun exclusionRegion(displayId: Int): Flow<Region?> {
+ return conflatedCallbackFlow {
+ val listener =
+ object : ISystemGestureExclusionListener.Stub() {
+ override fun onSystemGestureExclusionChanged(
+ displayId: Int,
+ restrictedRegion: Region?,
+ unrestrictedRegion: Region?,
+ ) {
+ trySend(restrictedRegion)
+ }
+ }
+ windowManager.registerSystemGestureExclusionListener(listener, displayId)
+
+ awaitClose {
+ windowManager.unregisterSystemGestureExclusionListener(listener, displayId)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt
new file mode 100644
index 0000000..4cee874
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractor.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.domain.interactor
+
+import android.graphics.Region
+import com.android.systemui.scene.data.repository.SystemGestureExclusionRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+class SystemGestureExclusionInteractor
+@Inject
+constructor(private val repository: SystemGestureExclusionRepository) {
+
+ /**
+ * Returns [Flow] of the [Region] in which system gestures should be excluded on the display
+ * identified with [displayId].
+ */
+ fun exclusionRegion(displayId: Int): Flow<Region?> {
+ return repository.exclusionRegion(displayId)
+ }
+}
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 b9f57f2..3c6d858 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
@@ -32,4 +32,7 @@
* normal collapse would.
*/
val SlightlyFasterShadeCollapse = TransitionKey("SlightlyFasterShadeCollapse")
+
+ /** Reference to a content transition that should happen instantly, i.e. without animation. */
+ val Instant = TransitionKey("Instant")
}
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 7f35d73..a7e7d8b 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
@@ -107,7 +107,9 @@
view.viewModel(
traceName = "SceneWindowRootViewBinder",
minWindowLifecycleState = WindowLifecycleState.ATTACHED,
- factory = { viewModelFactory.create(motionEventHandlerReceiver) },
+ factory = {
+ viewModelFactory.create(view.context.displayId, motionEventHandlerReceiver)
+ },
) { viewModel ->
try {
view.setViewTreeOnBackPressedDispatcherOwner(
@@ -184,7 +186,7 @@
PlatformTheme {
ScreenDecorProvider(
displayCutout = displayCutoutFromWindowInsets(scope, context, windowInsets),
- screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+ screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context),
) {
SceneContainer(
viewModel = viewModel,
@@ -205,9 +207,7 @@
): ComposeView {
return ComposeView(context).apply {
setContent {
- AlternateBouncer(
- alternateBouncerDependencies = alternateBouncerDependencies,
- )
+ AlternateBouncer(alternateBouncerDependencies = alternateBouncerDependencies)
}
}
}
@@ -234,14 +234,7 @@
else -> CutoutLocation.CENTER
}
val viewDisplayCutout = it?.displayCutout
- DisplayCutout(
- left,
- top,
- right,
- bottom,
- location,
- viewDisplayCutout,
- )
+ DisplayCutout(left, top, right, bottom, location, viewDisplayCutout)
}
.stateIn(scope, SharingStarted.WhileSubscribed(), DisplayCutout())
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.kt
new file mode 100644
index 0000000..a1d915a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerGestureFilter.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.viewmodel
+
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.geometry.Offset
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.scene.domain.interactor.SystemGestureExclusionInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlin.math.roundToInt
+
+/** Decides whether drag gestures should be filtered out in the scene container framework. */
+class SceneContainerGestureFilter
+@AssistedInject
+constructor(interactor: SystemGestureExclusionInteractor, @Assisted displayId: Int) :
+ ExclusiveActivatable() {
+
+ private val hydrator = Hydrator("SceneContainerGestureFilter.hydrator")
+ private val exclusionRegion by
+ hydrator.hydratedStateOf(
+ traceName = "exclusionRegion",
+ initialValue = null,
+ source = interactor.exclusionRegion(displayId),
+ )
+
+ override suspend fun onActivated(): Nothing {
+ hydrator.activate()
+ }
+
+ /**
+ * Returns `true` if a drag gesture starting at [startPosition] should be filtered out (e.g.
+ * ignored, `false` otherwise.
+ *
+ * Invoke this and pass in the position of the `ACTION_DOWN` pointer event that began the
+ * gesture.
+ */
+ fun shouldFilterGesture(startPosition: Offset): Boolean {
+ check(isActive) { "Must be activated to use!" }
+
+ return exclusionRegion?.contains(startPosition.x.roundToInt(), startPosition.y.roundToInt())
+ ?: false
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(displayId: Int): SceneContainerGestureFilter
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index af1f5a7..0bf2d49 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -18,6 +18,7 @@
import android.view.MotionEvent
import androidx.compose.runtime.getValue
+import androidx.compose.ui.geometry.Offset
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.DefaultEdgeDetector
import com.android.compose.animation.scene.ObservableTransitionState
@@ -41,9 +42,12 @@
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
/** Models UI state for the scene container. */
class SceneContainerViewModel
@@ -55,6 +59,8 @@
shadeInteractor: ShadeInteractor,
private val splitEdgeDetector: SplitEdgeDetector,
private val logger: SceneLogger,
+ gestureFilterFactory: SceneContainerGestureFilter.Factory,
+ @Assisted displayId: Int,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
@@ -80,6 +86,8 @@
},
)
+ private val gestureFilter: SceneContainerGestureFilter = gestureFilterFactory.create(displayId)
+
override suspend fun onActivated(): Nothing {
try {
// Sends a MotionEventHandler to the owner of the view-model so they can report
@@ -96,7 +104,11 @@
}
)
- hydrator.activate()
+ coroutineScope {
+ launch { hydrator.activate() }
+ launch { gestureFilter.activate() }
+ }
+ awaitCancellation()
} finally {
// Clears the previously-sent MotionEventHandler so the owner of the view-model releases
// their reference to it.
@@ -243,6 +255,17 @@
}
}
+ /**
+ * Returns `true` if a drag gesture starting at [startPosition] should be filtered out (e.g.
+ * ignored, `false` otherwise.
+ *
+ * Invoke this and pass in the position of the `ACTION_DOWN` pointer event that began the
+ * gesture.
+ */
+ fun shouldFilterGesture(startPosition: Offset): Boolean {
+ return gestureFilter.shouldFilterGesture(startPosition)
+ }
+
/** Defines interface for classes that can handle externally-reported [MotionEvent]s. */
interface MotionEventHandler {
/** Notifies that a [MotionEvent] has occurred. */
@@ -258,7 +281,8 @@
@AssistedFactory
interface Factory {
fun create(
- motionEventHandlerReceiver: (MotionEventHandler?) -> Unit
+ displayId: Int,
+ motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
): SceneContainerViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 361226a4..6c99282 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -22,8 +22,8 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.Instant
import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
import com.android.systemui.shade.ShadeController.ShadeVisibilityListener
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -80,18 +80,25 @@
}
}
+ @Deprecated("Deprecated in Java")
override fun isShadeEnabled() = shadeInteractor.isShadeEnabled.value
+ @Deprecated("Deprecated in Java")
override fun isShadeFullyOpen(): Boolean = shadeInteractor.isAnyFullyExpanded.value
+ @Deprecated("Deprecated in Java")
override fun isExpandingOrCollapsing(): Boolean = shadeInteractor.isUserInteracting.value
+ @Deprecated("Deprecated in Java")
override fun instantExpandShade() {
// Do nothing
}
override fun instantCollapseShade() {
- sceneInteractor.snapToScene(SceneFamilies.Home, "hide shade")
+ shadeInteractor.collapseNotificationsShade(
+ loggingReason = "ShadeControllerSceneImpl.instantCollapseShade",
+ transitionKey = Instant,
+ )
}
override fun animateCollapseShade(
@@ -122,16 +129,17 @@
}
}
+ @Deprecated("Deprecated in Java")
override fun collapseWithDuration(animationDuration: Int) {
// TODO(b/300258424) inline this. The only caller uses the default duration.
animateCollapseShade()
}
private fun animateCollapseShadeInternal() {
- sceneInteractor.changeScene(
- SceneFamilies.Home, // TODO(b/336581871): add sceneState?
- "ShadeController.animateCollapseShade",
- SlightlyFasterShadeCollapse,
+ // TODO(b/336581871): add sceneState?
+ shadeInteractor.collapseEitherShade(
+ loggingReason = "ShadeController.animateCollapseShade",
+ transitionKey = SlightlyFasterShadeCollapse,
)
}
@@ -140,6 +148,7 @@
animateCollapseShade()
}
+ @Deprecated("Deprecated in Java")
override fun closeShadeIfOpen(): Boolean {
if (shadeInteractor.isAnyExpanded.value) {
commandQueue.animateCollapsePanels(
@@ -155,6 +164,7 @@
animateCollapseShadeForcedDelayed()
}
+ @Deprecated("Deprecated in Java")
override fun collapseShade(animate: Boolean) {
if (animate) {
animateCollapseShade()
@@ -163,13 +173,14 @@
}
}
+ @Deprecated("Deprecated in Java")
override fun collapseOnMainThread() {
// TODO if this works with delegation alone, we can deprecate and delete
collapseShade()
}
override fun expandToNotifications() {
- shadeInteractor.expandNotificationShade("ShadeController.animateExpandShade")
+ shadeInteractor.expandNotificationsShade("ShadeController.animateExpandShade")
}
override fun expandToQs() {
@@ -193,14 +204,17 @@
}
}
+ @Deprecated("Deprecated in Java")
override fun postAnimateCollapseShade() {
animateCollapseShade()
}
+ @Deprecated("Deprecated in Java")
override fun postAnimateForceCollapseShade() {
animateCollapseShadeForced()
}
+ @Deprecated("Deprecated in Java")
override fun postAnimateExpandQs() {
expandToQs()
}
@@ -214,18 +228,23 @@
}
}
+ @Deprecated("Deprecated in Java")
override fun makeExpandedInvisible() {
// Do nothing
}
+ @Deprecated("Deprecated in Java")
override fun makeExpandedVisible(force: Boolean) {
// Do nothing
}
+ @Deprecated("Deprecated in Java")
override fun isExpandedVisible(): Boolean {
- return sceneInteractor.currentScene.value != Scenes.Gone
+ return sceneInteractor.currentScene.value != Scenes.Gone ||
+ sceneInteractor.currentOverlays.value.isNotEmpty()
}
+ @Deprecated("Deprecated in Java")
override fun onStatusBarTouch(event: MotionEvent) {
// The only call to this doesn't happen with MigrateClocksToBlueprint.isEnabled enabled
throw UnsupportedOperationException()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index b046c50..a3f2c64 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -17,6 +17,7 @@
package com.android.systemui.shade.domain.interactor
import androidx.annotation.FloatRange
+import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.shade.shared.model.ShadeMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -27,19 +28,22 @@
/** Business logic for shade interactions. */
interface ShadeInteractor : BaseShadeInteractor {
- /** Emits true if the shade is currently allowed and false otherwise. */
+ /** Emits true if the Notifications shade is currently allowed and false otherwise. */
val isShadeEnabled: StateFlow<Boolean>
- /** Emits true if QS is currently allowed and false otherwise. */
+ /** Emits true if QS shade is currently allowed and false otherwise. */
val isQsEnabled: StateFlow<Boolean>
- /** Whether either the shade or QS is fully expanded. */
+ /** Whether either the Notifications shade or QS shade is fully expanded. */
val isAnyFullyExpanded: StateFlow<Boolean>
- /** Whether the Shade is fully expanded. */
+ /** Whether the Notifications Shade is fully expanded. */
val isShadeFullyExpanded: Flow<Boolean>
- /** Whether the Shade is fully collapsed. */
+ /** Whether Notifications Shade is expanded a non-zero amount. */
+ val isShadeAnyExpanded: StateFlow<Boolean>
+
+ /** Whether the Notifications Shade is fully collapsed. */
val isShadeFullyCollapsed: Flow<Boolean>
/**
@@ -102,7 +106,7 @@
*/
val isAnyExpanded: StateFlow<Boolean>
- /** The amount [0-1] that the shade has been opened. */
+ /** The amount [0-1] that the Notifications Shade has been opened. */
val shadeExpansion: StateFlow<Float>
/**
@@ -111,7 +115,7 @@
*/
val qsExpansion: StateFlow<Float>
- /** Whether Quick Settings is expanded a non-zero amount. */
+ /** Whether Quick Settings Shade is expanded a non-zero amount. */
val isQsExpanded: StateFlow<Boolean>
/**
@@ -142,16 +146,38 @@
val isUserInteractingWithQs: Flow<Boolean>
/**
- * Triggers the expansion (opening) of the notification shade. If the notification shade is
- * already open, this has no effect.
+ * Triggers the expansion (opening) of the notifications shade. If it is already expanded, this
+ * has no effect.
*/
- fun expandNotificationShade(loggingReason: String)
+ fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey? = null)
/**
- * Triggers the expansion (opening) of the quick settings shade. If the quick settings shade is
- * already open, this has no effect.
+ * Triggers the expansion (opening) of the quick settings shade. If it is already expanded, this
+ * has no effect.
*/
- fun expandQuickSettingsShade(loggingReason: String)
+ fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey? = null)
+
+ /**
+ * Triggers the collapse (closing) of the notifications shade. If it is already collapsed, this
+ * has no effect.
+ */
+ fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey? = null)
+
+ /**
+ * Triggers the collapse (closing) of the quick settings shade. If it is already collapsed, this
+ * has no effect.
+ */
+ fun collapseQuickSettingsShade(
+ loggingReason: String,
+ transitionKey: TransitionKey? = null,
+ bypassNotificationsShade: Boolean = false,
+ )
+
+ /**
+ * Triggers the collapse (closing) of the notifications shade or quick settings shade, whichever
+ * is open. If both are already collapsed, this has no effect.
+ */
+ fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey? = null)
}
fun createAnyExpansionFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index fb14828..322fca3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade.domain.interactor
+import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.shade.shared.model.ShadeMode
import javax.inject.Inject
@@ -31,6 +32,7 @@
override val isShadeEnabled: StateFlow<Boolean> = inactiveFlowBoolean
override val isQsEnabled: StateFlow<Boolean> = inactiveFlowBoolean
override val shadeExpansion: StateFlow<Float> = inactiveFlowFloat
+ override val isShadeAnyExpanded: StateFlow<Boolean> = inactiveFlowBoolean
override val qsExpansion: StateFlow<Float> = inactiveFlowFloat
override val isQsExpanded: StateFlow<Boolean> = inactiveFlowBoolean
override val isQsBypassingShade: Flow<Boolean> = inactiveFlowBoolean
@@ -50,7 +52,17 @@
override fun getTopEdgeSplitFraction(): Float = 0.5f
- override fun expandNotificationShade(loggingReason: String) {}
+ override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {}
- override fun expandQuickSettingsShade(loggingReason: String) {}
+ override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {}
+
+ override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {}
+
+ override fun collapseQuickSettingsShade(
+ loggingReason: String,
+ transitionKey: TransitionKey?,
+ bypassNotificationsShade: Boolean,
+ ) {}
+
+ override fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey?) {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index 3eab02a..949d2aa 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -78,12 +78,16 @@
override val isShadeFullyExpanded: Flow<Boolean> =
baseShadeInteractor.shadeExpansion.map { it >= 1f }.distinctUntilChanged()
+ override val isShadeAnyExpanded: StateFlow<Boolean> =
+ baseShadeInteractor.shadeExpansion
+ .map { it > 0 }
+ .stateIn(scope, SharingStarted.Eagerly, false)
+
override val isShadeFullyCollapsed: Flow<Boolean> =
baseShadeInteractor.shadeExpansion.map { it <= 0f }.distinctUntilChanged()
override val isUserInteracting: StateFlow<Boolean> =
combine(isUserInteractingWithShade, isUserInteractingWithQs) { shade, qs -> shade || qs }
- .distinctUntilChanged()
.stateIn(scope, SharingStarted.Eagerly, false)
override val isShadeTouchable: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
index df09486..0902c39 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
@@ -17,6 +17,7 @@
package com.android.systemui.shade.domain.interactor
import com.android.app.tracing.FlowTracing.traceAsCounter
+import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardRepository
@@ -111,18 +112,40 @@
override val isUserInteractingWithQs: Flow<Boolean> =
userInteractingFlow(repository.legacyQsTracking, repository.qsExpansion)
- override fun expandNotificationShade(loggingReason: String) {
+ override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {
throw UnsupportedOperationException(
"expandNotificationShade() is not supported in legacy shade"
)
}
- override fun expandQuickSettingsShade(loggingReason: String) {
+ override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {
throw UnsupportedOperationException(
"expandQuickSettingsShade() is not supported in legacy shade"
)
}
+ override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {
+ throw UnsupportedOperationException(
+ "collapseNotificationShade() is not supported in legacy shade"
+ )
+ }
+
+ override fun collapseQuickSettingsShade(
+ loggingReason: String,
+ transitionKey: TransitionKey?,
+ bypassNotificationsShade: Boolean,
+ ) {
+ throw UnsupportedOperationException(
+ "collapseQuickSettingsShade() is not supported in legacy shade"
+ )
+ }
+
+ override fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey?) {
+ throw UnsupportedOperationException(
+ "collapseEitherShade() is not supported in legacy shade"
+ )
+ }
+
/**
* Return a flow for whether a user is interacting with an expandable shade component using
* tracking and expansion flows. NOTE: expansion must be a `StateFlow` to guarantee that
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
index 81bf712..7658108 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -21,12 +21,16 @@
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Overlays
+import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.Instant
+import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import javax.inject.Inject
@@ -133,43 +137,120 @@
}
}
- override fun expandNotificationShade(loggingReason: String) {
+ override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {
if (shadeModeInteractor.isDualShade) {
if (Overlays.QuickSettingsShade in sceneInteractor.currentOverlays.value) {
sceneInteractor.replaceOverlay(
from = Overlays.QuickSettingsShade,
to = Overlays.NotificationsShade,
loggingReason = loggingReason,
+ transitionKey = transitionKey,
)
} else {
sceneInteractor.showOverlay(
overlay = Overlays.NotificationsShade,
loggingReason = loggingReason,
+ transitionKey = transitionKey,
)
}
} else {
- sceneInteractor.changeScene(toScene = Scenes.Shade, loggingReason = loggingReason)
+ sceneInteractor.changeScene(
+ toScene = Scenes.Shade,
+ loggingReason = loggingReason,
+ transitionKey =
+ transitionKey ?: ToSplitShade.takeIf { shadeModeInteractor.isSplitShade },
+ )
}
}
- override fun expandQuickSettingsShade(loggingReason: String) {
+ override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {
if (shadeModeInteractor.isDualShade) {
if (Overlays.NotificationsShade in sceneInteractor.currentOverlays.value) {
sceneInteractor.replaceOverlay(
from = Overlays.NotificationsShade,
to = Overlays.QuickSettingsShade,
loggingReason = loggingReason,
+ transitionKey = transitionKey,
)
} else {
sceneInteractor.showOverlay(
overlay = Overlays.QuickSettingsShade,
loggingReason = loggingReason,
+ transitionKey = transitionKey,
)
}
} else {
+ val isSplitShade = shadeModeInteractor.isSplitShade
sceneInteractor.changeScene(
- toScene = Scenes.QuickSettings,
+ toScene = if (isSplitShade) Scenes.Shade else Scenes.QuickSettings,
loggingReason = loggingReason,
+ transitionKey = transitionKey ?: ToSplitShade.takeIf { isSplitShade },
+ )
+ }
+ }
+
+ override fun collapseNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {
+ if (shadeModeInteractor.isDualShade) {
+ // TODO(b/356596436): Hide without animation if transitionKey is Instant.
+ sceneInteractor.hideOverlay(
+ overlay = Overlays.NotificationsShade,
+ loggingReason = loggingReason,
+ transitionKey = transitionKey,
+ )
+ } else if (transitionKey == Instant) {
+ // TODO(b/356596436): Define instant transition instead of snapToScene().
+ sceneInteractor.snapToScene(toScene = SceneFamilies.Home, loggingReason = loggingReason)
+ } else {
+ sceneInteractor.changeScene(
+ toScene = SceneFamilies.Home,
+ loggingReason = loggingReason,
+ transitionKey =
+ transitionKey ?: ToSplitShade.takeIf { shadeModeInteractor.isSplitShade },
+ )
+ }
+ }
+
+ override fun collapseQuickSettingsShade(
+ loggingReason: String,
+ transitionKey: TransitionKey?,
+ bypassNotificationsShade: Boolean,
+ ) {
+ if (shadeModeInteractor.isDualShade) {
+ // TODO(b/356596436): Hide without animation if transitionKey is Instant.
+ sceneInteractor.hideOverlay(
+ overlay = Overlays.QuickSettingsShade,
+ loggingReason = loggingReason,
+ transitionKey = transitionKey,
+ )
+ return
+ }
+
+ val isSplitShade = shadeModeInteractor.isSplitShade
+ val targetScene =
+ if (bypassNotificationsShade || isSplitShade) SceneFamilies.Home else Scenes.Shade
+ if (transitionKey == Instant) {
+ // TODO(b/356596436): Define instant transition instead of snapToScene().
+ sceneInteractor.snapToScene(toScene = targetScene, loggingReason = loggingReason)
+ } else {
+ sceneInteractor.changeScene(
+ toScene = targetScene,
+ loggingReason = loggingReason,
+ transitionKey = transitionKey ?: ToSplitShade.takeIf { isSplitShade },
+ )
+ }
+ }
+
+ override fun collapseEitherShade(loggingReason: String, transitionKey: TransitionKey?) {
+ // Note: The notifications shade and QS shade may be both partially expanded simultaneously,
+ // so we don't use an 'else' clause here.
+ if (shadeExpansion.value > 0) {
+ collapseNotificationsShade(loggingReason = loggingReason, transitionKey = transitionKey)
+ }
+ if (isQsExpanded.value) {
+ collapseQuickSettingsShade(
+ loggingReason = loggingReason,
+ transitionKey = transitionKey,
+ bypassNotificationsShade = true,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
index 0fb3790..ea76ac4b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt
@@ -21,10 +21,9 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.Instant
import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.shade.shared.model.ShadeMode
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -48,7 +47,7 @@
@Deprecated("Use ShadeInteractor instead")
override fun expandToNotifications() {
- shadeInteractor.expandNotificationShade(
+ shadeInteractor.expandNotificationsShade(
loggingReason = "ShadeLockscreenInteractorImpl.expandToNotifications"
)
}
@@ -71,17 +70,11 @@
}
override fun resetViews(animate: Boolean) {
- val loggingReason = "ShadeLockscreenInteractorImpl.resetViews"
// The existing comment to the only call to this claims it only calls it to collapse QS
- if (shadeInteractor.shadeMode.value == ShadeMode.Dual) {
- // TODO(b/356596436): Hide without animation if !animate.
- sceneInteractor.hideOverlay(
- overlay = Overlays.QuickSettingsShade,
- loggingReason = loggingReason,
- )
- } else {
- shadeInteractor.expandNotificationShade(loggingReason)
- }
+ shadeInteractor.collapseQuickSettingsShade(
+ loggingReason = "ShadeLockscreenInteractorImpl.resetViews",
+ transitionKey = Instant.takeIf { !animate },
+ )
}
@Deprecated("Not supported by scenes")
@@ -93,7 +86,7 @@
backgroundScope.launch {
delay(delay)
withContext(mainDispatcher) {
- shadeInteractor.expandNotificationShade(
+ shadeInteractor.expandNotificationsShade(
"ShadeLockscreenInteractorImpl.transitionToExpandedShade"
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
index caa4513..c838c37 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeModeInteractor.kt
@@ -55,6 +55,10 @@
val isDualShade: Boolean
get() = shadeMode.value is ShadeMode.Dual
+ /** Convenience shortcut for querying whether the current [shadeMode] is [ShadeMode.Split]. */
+ val isSplitShade: Boolean
+ get() = shadeMode.value is ShadeMode.Split
+
/**
* The fraction between [0..1] (i.e., percentage) of screen width to consider the threshold
* between "top-left" and "top-right" for the purposes of dual-shade invocation.
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index a154e91..bd4ed5b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -29,9 +29,7 @@
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.SceneFamilies
-import com.android.systemui.scene.shared.model.TransitionKeys
+import com.android.systemui.scene.shared.model.TransitionKeys.SlightlyFasterShadeCollapse
import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor
import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -55,9 +53,8 @@
class ShadeHeaderViewModel
@AssistedInject
constructor(
- private val context: Context,
+ context: Context,
private val activityStarter: ActivityStarter,
- private val sceneInteractor: SceneInteractor,
private val shadeInteractor: ShadeInteractor,
private val mobileIconsInteractor: MobileIconsInteractor,
val mobileIconsViewModel: MobileIconsViewModel,
@@ -120,7 +117,7 @@
map = { intent, _ ->
intent.action == Intent.ACTION_TIMEZONE_CHANGED ||
intent.action == Intent.ACTION_LOCALE_CHANGED
- }
+ },
)
.onEach { invalidateFormats -> updateDateTexts(invalidateFormats) }
.launchIn(this)
@@ -152,10 +149,9 @@
/** Notifies that the system icons container was clicked. */
fun onSystemIconContainerClicked() {
- sceneInteractor.changeScene(
- SceneFamilies.Home,
- "ShadeHeaderViewModel.onSystemIconContainerClicked",
- TransitionKeys.SlightlyFasterShadeCollapse,
+ shadeInteractor.collapseEitherShade(
+ loggingReason = "ShadeHeaderViewModel.onSystemIconContainerClicked",
+ transitionKey = SlightlyFasterShadeCollapse,
)
}
@@ -163,7 +159,7 @@
fun onShadeCarrierGroupClicked() {
activityStarter.postStartActivityDismissingKeyguard(
Intent(Settings.ACTION_WIRELESS_SETTINGS),
- 0
+ 0,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
index 6907eef..1c840e0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/HeadsUpManagerPhone.java
@@ -103,8 +103,7 @@
private boolean mTrackingHeadsUp;
private final HashSet<String> mSwipedOutKeys = new HashSet<>();
private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>();
- @VisibleForTesting
- public final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
+ private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed
= new ArraySet<>();
private boolean mIsShadeOrQsExpanded;
private boolean mIsQsExpanded;
@@ -428,7 +427,7 @@
for (NotificationEntry entry : mEntriesToRemoveWhenReorderingAllowed) {
if (isHeadsUpEntry(entry.getKey())) {
// Maybe the heads-up was removed already
- removeEntry(entry.getKey(), "allowReorder");
+ removeEntry(entry.getKey(), "mOnReorderingAllowedListener");
}
}
mEntriesToRemoveWhenReorderingAllowed.clear();
@@ -631,8 +630,11 @@
super.setEntry(entry, removeRunnable);
if (NotificationThrottleHun.isEnabled()) {
- mEntriesToRemoveWhenReorderingAllowed.add(entry);
- if (!mVisualStabilityProvider.isReorderingAllowed()) {
+ if (!mVisualStabilityProvider.isReorderingAllowed()
+ // We don't want to allow reordering while pulsing, but headsup need to
+ // time out anyway
+ && !entry.showingPulsing()) {
+ mEntriesToRemoveWhenReorderingAllowed.add(entry);
entry.setSeenInShade(true);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
index ec3c7d0..0f93b5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationActivityStarter.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,37 +13,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.systemui.statusbar.notification
-package com.android.systemui.statusbar.notification;
-
-import android.content.Intent;
-import android.view.View;
-
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import android.content.Intent
+import android.view.View
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
/**
* Component responsible for handling actions on a notification which cause activites to start.
* (e.g. clicking on a notification, tapping on the settings icon in the notification guts)
*/
-public interface NotificationActivityStarter {
+interface NotificationActivityStarter {
/** Called when the user clicks on the notification bubble icon. */
- void onNotificationBubbleIconClicked(NotificationEntry entry);
+ fun onNotificationBubbleIconClicked(entry: NotificationEntry?)
/** Called when the user clicks on the surface of a notification. */
- void onNotificationClicked(NotificationEntry entry, ExpandableNotificationRow row);
+ fun onNotificationClicked(entry: NotificationEntry?, row: ExpandableNotificationRow?)
/** Called when the user clicks on a button in the notification guts which fires an intent. */
- void startNotificationGutsIntent(Intent intent, int appUid,
- ExpandableNotificationRow row);
+ fun startNotificationGutsIntent(intent: Intent?, appUid: Int, row: ExpandableNotificationRow?)
- /** Called when the user clicks "Manage" or "History" in the Shade. */
- void startHistoryIntent(View view, boolean showHistory);
+ /**
+ * Called when the user clicks "Manage" or "History" in the Shade, or the "No notifications"
+ * text.
+ */
+ fun startHistoryIntent(view: View?, showHistory: Boolean)
/** Called when the user succeed to drop notification to proper target view. */
- void onDragSuccess(NotificationEntry entry);
+ fun onDragSuccess(entry: NotificationEntry?)
- default boolean isCollapsingToShowActivityOverLockscreen() {
- return false;
- }
+ val isCollapsingToShowActivityOverLockscreen: Boolean
+ get() = false
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
index b342722..b67092c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManager.kt
@@ -22,7 +22,7 @@
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.NotificationClassificationFlag
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
import com.android.systemui.statusbar.notification.stack.BUCKET_FOREGROUND_SERVICE
@@ -54,7 +54,7 @@
fun getNotificationBuckets(): IntArray {
if (
PriorityPeopleSection.isEnabled ||
- NotificationMinimalismPrototype.isEnabled ||
+ NotificationMinimalism.isEnabled ||
NotificationClassificationFlag.isEnabled
) {
// We don't need this list to be adaptive, it can be the superset of all features.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
index a621b2a..4e63b92 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -35,7 +35,7 @@
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_ONGOING
import com.android.systemui.statusbar.notification.stack.BUCKET_TOP_UNSEEN
import com.android.systemui.util.asIndenting
@@ -77,7 +77,7 @@
private var unseenFilterEnabled = false
override fun attach(pipeline: NotifPipeline) {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) {
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) {
return
}
pipeline.addPromoter(unseenNotifPromoter)
@@ -129,26 +129,25 @@
}
}
- private fun unseenFeatureEnabled(): Flow<Boolean> {
- // TODO(b/330387368): create LOCK_SCREEN_NOTIFICATION_MINIMALISM setting to use here?
- // Or should we actually just repurpose using the existing setting?
- if (NotificationMinimalismPrototype.isEnabled) {
- return flowOf(true)
+ private fun minimalismFeatureSettingEnabled(): Flow<Boolean> {
+ if (!NotificationMinimalism.isEnabled) {
+ return flowOf(false)
}
- return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled()
+ return seenNotificationsInteractor.isLockScreenNotificationMinimalismEnabled()
}
private suspend fun trackUnseenFilterSettingChanges() {
- unseenFeatureEnabled().collectLatest { isSettingEnabled ->
+ // Only filter the seen notifs when the lock screen minimalism feature settings is on.
+ minimalismFeatureSettingEnabled().collectLatest { isMinimalismSettingEnabled ->
// update local field and invalidate if necessary
- if (isSettingEnabled != unseenFilterEnabled) {
- unseenFilterEnabled = isSettingEnabled
+ if (isMinimalismSettingEnabled != unseenFilterEnabled) {
+ unseenFilterEnabled = isMinimalismSettingEnabled
unseenNotifications.clear()
unseenNotifPromoter.invalidateList("unseen setting changed")
}
// if the setting is enabled, then start tracking and filtering unseen notifications
- logger.logTrackingUnseen(isSettingEnabled)
- if (isSettingEnabled) {
+ logger.logTrackingUnseen(isMinimalismSettingEnabled)
+ if (isMinimalismSettingEnabled) {
trackSeenNotifications()
}
}
@@ -178,7 +177,7 @@
}
private fun pickOutTopUnseenNotifs(list: List<ListEntry>) {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
if (!unseenFilterEnabled) return
// Only ever elevate a top unseen notification on keyguard, not even locked shade
if (statusBarStateController.state != StatusBarState.KEYGUARD) {
@@ -215,9 +214,9 @@
object : NotifPromoter(TAG) {
override fun shouldPromoteToTopLevel(child: NotificationEntry): Boolean =
when {
- NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode() -> false
+ NotificationMinimalism.isUnexpectedlyInLegacyMode() -> false
seenNotificationsInteractor.isTopOngoingNotification(child) -> true
- !NotificationMinimalismPrototype.ungroupTopUnseen -> false
+ !NotificationMinimalism.ungroupTopUnseen -> false
else -> seenNotificationsInteractor.isTopUnseenNotification(child)
}
}
@@ -225,7 +224,7 @@
val topOngoingSectioner =
object : NotifSectioner("TopOngoing", BUCKET_TOP_ONGOING) {
override fun isInSection(entry: ListEntry): Boolean {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false
return entry.anyEntry { notificationEntry ->
seenNotificationsInteractor.isTopOngoingNotification(notificationEntry)
}
@@ -235,7 +234,7 @@
val topUnseenSectioner =
object : NotifSectioner("TopUnseen", BUCKET_TOP_UNSEEN) {
override fun isInSection(entry: ListEntry): Boolean {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return false
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return false
return entry.anyEntry { notificationEntry ->
seenNotificationsInteractor.isTopUnseenNotification(notificationEntry)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
index e44a77c..a4fa729 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorLogger.kt
@@ -34,7 +34,10 @@
TAG,
LogLevel.DEBUG,
messageInitializer = { bool1 = trackingUnseen },
- messagePrinter = { "${if (bool1) "Start" else "Stop"} tracking unseen notifications." },
+ messagePrinter = {
+ "${if (bool1) "Start" else "Stop"} " +
+ "tracking unseen notifications because of settings change."
+ },
)
fun logShadeVisible(numUnseen: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
index 0bbde21..82ce31b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
@@ -25,7 +25,6 @@
import androidx.annotation.NonNull;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.Flags;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -61,8 +60,27 @@
return false;
}
- if (!Flags.notificationsBackgroundIcons()) {
- inflateOrUpdateIcons(entry);
+ switch (mIconsState.getOrDefault(entry, STATE_ICONS_UNINFLATED)) {
+ case STATE_ICONS_UNINFLATED:
+ try {
+ mIconManager.createIcons(entry);
+ mIconsState.put(entry, STATE_ICONS_INFLATED);
+ } catch (InflationException e) {
+ reportInflationError(entry, e);
+ mIconsState.put(entry, STATE_ICONS_ERROR);
+ }
+ break;
+ case STATE_ICONS_INFLATED:
+ try {
+ mIconManager.updateIcons(entry, /* usingCache = */ false);
+ } catch (InflationException e) {
+ reportInflationError(entry, e);
+ mIconsState.put(entry, STATE_ICONS_ERROR);
+ }
+ break;
+ case STATE_ICONS_ERROR:
+ // do nothing
+ break;
}
return true;
@@ -72,19 +90,7 @@
private final NotifCollectionListener mCollectionListener = new NotifCollectionListener() {
@Override
public void onEntryInit(@NonNull NotificationEntry entry) {
- // We default to STATE_ICONS_UNINFLATED anyway, so there's no need to initialize it.
- if (!Flags.notificationsBackgroundIcons()) {
- mIconsState.put(entry, STATE_ICONS_UNINFLATED);
- }
- }
-
- @Override
- public void onEntryAdded(@NonNull NotificationEntry entry) {
- if (Flags.notificationsBackgroundIcons()) {
- if (isMediaNotification(entry.getSbn())) {
- inflateOrUpdateIcons(entry);
- }
- }
+ mIconsState.put(entry, STATE_ICONS_UNINFLATED);
}
@Override
@@ -93,12 +99,6 @@
// The update may have fixed the inflation error, so give it another chance.
mIconsState.put(entry, STATE_ICONS_UNINFLATED);
}
-
- if (Flags.notificationsBackgroundIcons()) {
- if (isMediaNotification(entry.getSbn())) {
- inflateOrUpdateIcons(entry);
- }
- }
}
@Override
@@ -107,31 +107,6 @@
}
};
- private void inflateOrUpdateIcons(NotificationEntry entry) {
- switch (mIconsState.getOrDefault(entry, STATE_ICONS_UNINFLATED)) {
- case STATE_ICONS_UNINFLATED:
- try {
- mIconManager.createIcons(entry);
- mIconsState.put(entry, STATE_ICONS_INFLATED);
- } catch (InflationException e) {
- reportInflationError(entry, e);
- mIconsState.put(entry, STATE_ICONS_ERROR);
- }
- break;
- case STATE_ICONS_INFLATED:
- try {
- mIconManager.updateIcons(entry, /* usingCache = */ false);
- } catch (InflationException e) {
- reportInflationError(entry, e);
- mIconsState.put(entry, STATE_ICONS_ERROR);
- }
- break;
- case STATE_ICONS_ERROR:
- // do nothing
- break;
- }
- }
-
private void reportInflationError(NotificationEntry entry, Exception e) {
// This is the same logic as in PreparationCoordinator; it doesn't handle media
// notifications when the media feature is enabled since they aren't displayed in the shade,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index 73ce48b..96c260b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -25,7 +25,7 @@
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import javax.inject.Inject
@@ -88,11 +88,10 @@
mCoordinators.add(hideLocallyDismissedNotifsCoordinator)
mCoordinators.add(hideNotifsForOtherUsersCoordinator)
mCoordinators.add(keyguardCoordinator)
- if (NotificationMinimalismPrototype.isEnabled) {
+ if (NotificationMinimalism.isEnabled) {
mCoordinators.add(lockScreenMinimalismCoordinator)
- } else {
- mCoordinators.add(unseenKeyguardCoordinator)
}
+ mCoordinators.add(unseenKeyguardCoordinator)
mCoordinators.add(rankingCoordinator)
mCoordinators.add(colorizedFgsCoordinator)
mCoordinators.add(deviceProvisionedCoordinator)
@@ -125,11 +124,11 @@
}
// Manually add Ordered Sections
- if (NotificationMinimalismPrototype.isEnabled) {
+ if (NotificationMinimalism.isEnabled) {
mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
}
mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp
- if (NotificationMinimalismPrototype.isEnabled) {
+ if (NotificationMinimalism.isEnabled) {
mOrderedSections.add(lockScreenMinimalismCoordinator.topUnseenSectioner) // Top Unseen
}
mOrderedSections.add(colorizedFgsCoordinator.sectioner) // ForegroundService
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
index bfea2ba..cf1329c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/OriginalUnseenKeyguardCoordinator.kt
@@ -35,7 +35,6 @@
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
import com.android.systemui.statusbar.policy.HeadsUpManager
import com.android.systemui.statusbar.policy.headsUpEvents
import com.android.systemui.util.asIndenting
@@ -51,7 +50,6 @@
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -87,7 +85,6 @@
private var unseenFilterEnabled = false
override fun attach(pipeline: NotifPipeline) {
- NotificationMinimalismPrototype.assertInLegacyMode()
pipeline.addFinalizeFilter(unseenNotifFilter)
pipeline.addCollectionListener(collectionListener)
scope.launch { trackUnseenFilterSettingChanges() }
@@ -253,10 +250,6 @@
}
private fun unseenFeatureEnabled(): Flow<Boolean> {
- if (NotificationMinimalismPrototype.isEnabled) {
- // TODO(b/330387368): should this really just be turned off? If so, hide the setting.
- return flowOf(false)
- }
return seenNotificationsInteractor.isLockScreenShowOnlyUnseenNotificationsEnabled()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
index fe59d73..5ff5d2d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -45,17 +45,16 @@
/**
* How long to wait before auto-dismissing a notification that was kept for active remote input, and
- * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel
- * these given that they technically don't exist anymore. We wait a bit in case the app issues
- * an update, and to also give the other lifetime extenders a beat to decide they want it.
+ * has now sent a remote input. We auto-dismiss, because the app may not cannot cancel these given
+ * that they technically don't exist anymore. We wait a bit in case the app issues an update, and to
+ * also give the other lifetime extenders a beat to decide they want it.
*/
private const val REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY: Long = 500
/**
* How long to wait before releasing a lifetime extension when requested to do so due to a user
- * interaction (such as tapping another action).
- * We wait a bit in case the app issues an update in response to the action, but not too long or we
- * risk appearing unresponsive to the user.
+ * interaction (such as tapping another action). We wait a bit in case the app issues an update in
+ * response to the action, but not too long or we risk appearing unresponsive to the user.
*/
private const val REMOTE_INPUT_EXTENDER_RELEASE_DELAY: Long = 200
@@ -63,22 +62,21 @@
private val DEBUG: Boolean by lazy { Log.isLoggable(TAG, Log.DEBUG) }
@CoordinatorScope
-class RemoteInputCoordinator @Inject constructor(
+class RemoteInputCoordinator
+@Inject
+constructor(
dumpManager: DumpManager,
private val mRebuilder: RemoteInputNotificationRebuilder,
private val mNotificationRemoteInputManager: NotificationRemoteInputManager,
@Main private val mMainHandler: Handler,
- private val mSmartReplyController: SmartReplyController
+ private val mSmartReplyController: SmartReplyController,
) : Coordinator, RemoteInputListener, Dumpable {
@VisibleForTesting val mRemoteInputHistoryExtender = RemoteInputHistoryExtender()
@VisibleForTesting val mSmartReplyHistoryExtender = SmartReplyHistoryExtender()
@VisibleForTesting val mRemoteInputActiveExtender = RemoteInputActiveExtender()
- private val mRemoteInputLifetimeExtenders = listOf(
- mRemoteInputHistoryExtender,
- mSmartReplyHistoryExtender,
- mRemoteInputActiveExtender
- )
+ private val mRemoteInputLifetimeExtenders =
+ listOf(mRemoteInputHistoryExtender, mSmartReplyHistoryExtender, mRemoteInputActiveExtender)
private lateinit var mNotifUpdater: InternalNotifUpdater
@@ -93,9 +91,7 @@
if (lifetimeExtensionRefactor()) {
pipeline.addNotificationLifetimeExtender(mRemoteInputActiveExtender)
} else {
- mRemoteInputLifetimeExtenders.forEach {
- pipeline.addNotificationLifetimeExtender(it)
- }
+ mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) }
}
mNotifUpdater = pipeline.getInternalNotifUpdater(TAG)
pipeline.addCollectionListener(mCollectionListener)
@@ -105,65 +101,86 @@
* Listener that updates the appearance of the notification if it has been lifetime extended
* by a a direct reply or a smart reply, and cancelled.
*/
- val mCollectionListener = object : NotifCollectionListener {
- override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
- if (DEBUG) {
- Log.d(TAG, "mCollectionListener.onEntryUpdated(entry=${entry.key}," +
- " fromSystem=$fromSystem)")
- }
- if (fromSystem) {
- if (lifetimeExtensionRefactor()) {
- if ((entry.getSbn().getNotification().flags
- and FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
- if (mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(
- entry)) {
- val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
- entry.onRemoteInputInserted()
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
- "Extending lifetime of notification with remote input")
- } else if (mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
- entry)) {
- val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
- mSmartReplyController.stopSending(entry)
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
- "Extending lifetime of notification with smart reply")
- } else {
- // The app may have re-cancelled a notification after it had already
- // been lifetime extended.
- // Rebuild the notification with the replies it already had to ensure
- // those replies continue to be displayed.
- val newSbn = mRebuilder.rebuildWithExistingReplies(entry)
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
+ val mCollectionListener =
+ object : NotifCollectionListener {
+ override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "mCollectionListener.onEntryUpdated(entry=${entry.key}," +
+ " fromSystem=$fromSystem)",
+ )
+ }
+ if (fromSystem) {
+ if (lifetimeExtensionRefactor()) {
+ if (
+ (entry.getSbn().getNotification().flags and
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0
+ ) {
+ if (
+ mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(
+ entry
+ )
+ ) {
+ val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
+ entry.onRemoteInputInserted()
+ mNotifUpdater.onInternalNotificationUpdate(
+ newSbn,
+ "Extending lifetime of notification with remote input",
+ )
+ } else if (
+ mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
+ entry
+ )
+ ) {
+ val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
+ mSmartReplyController.stopSending(entry)
+ mNotifUpdater.onInternalNotificationUpdate(
+ newSbn,
+ "Extending lifetime of notification with smart reply",
+ )
+ } else {
+ // The app may have re-cancelled a notification after it had already
+ // been lifetime extended.
+ // Rebuild the notification with the replies it already had to
+ // ensure
+ // those replies continue to be displayed.
+ val newSbn = mRebuilder.rebuildWithExistingReplies(entry)
+ mNotifUpdater.onInternalNotificationUpdate(
+ newSbn,
"Extending lifetime of notification that has already been " +
- "lifetime extended.")
+ "lifetime extended.",
+ )
+ }
+ } else {
+ // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
+ // should have their remote inputs list cleared.
+ entry.remoteInputs = null
}
} else {
- // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
- // should have their remote inputs list cleared.
- entry.remoteInputs = null
+ // Mark smart replies as sent whenever a notification is updated by the app,
+ // otherwise the smart replies are never marked as sent.
+ mSmartReplyController.stopSending(entry)
}
- } else {
- // Mark smart replies as sent whenever a notification is updated by the app,
- // otherwise the smart replies are never marked as sent.
- mSmartReplyController.stopSending(entry)
+ }
+ }
+
+ override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
+ if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})")
+ // We're removing the notification, the smart reply controller can forget about it.
+ // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear
+ // it.
+ mSmartReplyController.stopSending(entry)
+
+ // When we know the entry will not be lifetime extended, clean up the remote input
+ // view
+ // TODO: Share code with NotifCollection.cannotBeLifetimeExtended
+ if (reason == REASON_CANCEL || reason == REASON_CLICK) {
+ mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry)
}
}
}
- override fun onEntryRemoved(entry: NotificationEntry, reason: Int) {
- if (DEBUG) Log.d(TAG, "mCollectionListener.onEntryRemoved(entry=${entry.key})")
- // We're removing the notification, the smart reply controller can forget about it.
- // TODO(b/145659174): track 'sending' state on the entry to avoid having to clear it.
- mSmartReplyController.stopSending(entry)
-
- // When we know the entry will not be lifetime extended, clean up the remote input view
- // TODO: Share code with NotifCollection.cannotBeLifetimeExtended
- if (reason == REASON_CANCEL || reason == REASON_CLICK) {
- mNotificationRemoteInputManager.cleanUpRemoteInputForUserRemoval(entry)
- }
- }
- }
-
override fun dump(pw: PrintWriter, args: Array<out String>) {
mRemoteInputLifetimeExtenders.forEach { it.dump(pw, args) }
}
@@ -183,22 +200,25 @@
// view it is already canceled, so we'll need to cancel it on the apps behalf
// now that a reply has been sent. However, delay so that the app has time to posts an
// update in the mean time, and to give another lifetime extender time to pick it up.
- mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY)
+ mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
+ entry.key,
+ REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY,
+ )
}
private fun onSmartReplySent(entry: NotificationEntry, reply: CharSequence) {
if (DEBUG) Log.d(TAG, "onSmartReplySent(entry=${entry.key})")
val newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply)
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
- "Adding smart reply spinner for sent")
+ mNotifUpdater.onInternalNotificationUpdate(newSbn, "Adding smart reply spinner for sent")
// If we're extending for remote input being active, then from the apps point of
// view it is already canceled, so we'll need to cancel it on the apps behalf
// now that a reply has been sent. However, delay so that the app has time to posts an
// update in the mean time, and to give another lifetime extender time to pick it up.
- mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY)
+ mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
+ entry.key,
+ REMOTE_INPUT_ACTIVE_EXTENDER_AUTO_CANCEL_DELAY,
+ )
}
override fun onPanelCollapsed() {
@@ -208,19 +228,25 @@
override fun isNotificationKeptForRemoteInputHistory(key: String) =
if (!lifetimeExtensionRefactor()) {
mRemoteInputHistoryExtender.isExtending(key) ||
- mSmartReplyHistoryExtender.isExtending(key)
+ mSmartReplyHistoryExtender.isExtending(key)
} else false
override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
if (!lifetimeExtensionRefactor()) {
- mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
- mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(
+ entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
+ )
+ mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(
+ entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
+ )
}
- mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
+ entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
+ )
}
override fun setRemoteInputController(remoteInputController: RemoteInputController) {
@@ -229,32 +255,36 @@
@VisibleForTesting
inner class RemoteInputHistoryExtender :
- SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) {
+ SelfTrackingLifetimeExtender(TAG, "RemoteInputHistory", DEBUG, mMainHandler) {
override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
- mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry)
+ mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(entry)
override fun onStartedLifetimeExtension(entry: NotificationEntry) {
val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
entry.onRemoteInputInserted()
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
- "Extending lifetime of notification with remote input")
+ mNotifUpdater.onInternalNotificationUpdate(
+ newSbn,
+ "Extending lifetime of notification with remote input",
+ )
// TODO: Check if the entry was removed due perhaps to an inflation exception?
}
}
@VisibleForTesting
inner class SmartReplyHistoryExtender :
- SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) {
+ SelfTrackingLifetimeExtender(TAG, "SmartReplyHistory", DEBUG, mMainHandler) {
override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
- mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry)
+ mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(entry)
override fun onStartedLifetimeExtension(entry: NotificationEntry) {
val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
mSmartReplyController.stopSending(entry)
- mNotifUpdater.onInternalNotificationUpdate(newSbn,
- "Extending lifetime of notification with smart reply")
+ mNotifUpdater.onInternalNotificationUpdate(
+ newSbn,
+ "Extending lifetime of notification with smart reply",
+ )
// TODO: Check if the entry was removed due perhaps to an inflation exception?
}
@@ -266,9 +296,9 @@
@VisibleForTesting
inner class RemoteInputActiveExtender :
- SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) {
+ SelfTrackingLifetimeExtender(TAG, "RemoteInputActive", DEBUG, mMainHandler) {
override fun queryShouldExtendLifetime(entry: NotificationEntry): Boolean =
- mNotificationRemoteInputManager.isRemoteInputActive(entry)
+ mNotificationRemoteInputManager.isRemoteInputActive(entry)
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
index 6d0148a..41419f3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinator.java
@@ -41,7 +41,7 @@
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype;
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.kotlin.BooleanFlowOperators;
@@ -170,7 +170,7 @@
if (entry == null) {
return false;
}
- boolean isTopUnseen = NotificationMinimalismPrototype.isEnabled()
+ boolean isTopUnseen = NotificationMinimalism.isEnabled()
&& (mSeenNotificationsInteractor.isTopUnseenNotification(entry)
|| mSeenNotificationsInteractor.isTopOngoingNotification(entry));
if (isTopUnseen || mHeadsUpManager.isHeadsUpEntry(entry.getKey())) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
index 2956432..1babe47 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractor.kt
@@ -23,7 +23,7 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.util.printSection
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
@@ -57,25 +57,25 @@
/** Set the entry that is identified as the top ongoing notification. */
fun setTopOngoingNotification(entry: NotificationEntry?) {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
notificationListRepository.topOngoingNotificationKey.value = entry?.key
}
/** Determine if the given notification is the top ongoing notification. */
fun isTopOngoingNotification(entry: NotificationEntry?): Boolean =
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false
else
entry != null && notificationListRepository.topOngoingNotificationKey.value == entry.key
/** Set the entry that is identified as the top unseen notification. */
fun setTopUnseenNotification(entry: NotificationEntry?) {
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) return
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) return
notificationListRepository.topUnseenNotificationKey.value = entry?.key
}
/** Determine if the given notification is the top unseen notification. */
fun isTopUnseenNotification(entry: NotificationEntry?): Boolean =
- if (NotificationMinimalismPrototype.isUnexpectedlyInLegacyMode()) false
+ if (NotificationMinimalism.isUnexpectedlyInLegacyMode()) false
else entry != null && notificationListRepository.topUnseenNotificationKey.value == entry.key
fun dump(pw: IndentingPrintWriter) =
@@ -120,4 +120,29 @@
// only track the most recent emission, if events are happening faster than they can be
// consumed
.conflate()
+
+ fun isLockScreenNotificationMinimalismEnabled(): Flow<Boolean> =
+ secureSettings
+ // emit whenever the setting has changed
+ .observerFlow(
+ UserHandle.USER_ALL,
+ Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+ )
+ // perform a query immediately
+ .onStart { emit(Unit) }
+ // for each change, lookup the new value
+ .map {
+ secureSettings.getIntForUser(
+ name = Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+ default = 1,
+ userHandle = UserHandle.USER_CURRENT,
+ ) == 1
+ }
+ // don't emit anything if nothing has changed
+ .distinctUntilChanged()
+ // perform lookups on the bg thread pool
+ .flowOn(bgDispatcher)
+ // only track the most recent emission, if events are happening faster than they can be
+ // consumed
+ .conflate()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
similarity index 65%
copy from packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
copy to packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
index 06f3db5..f1fc275 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
@@ -14,39 +14,26 @@
* limitations under the License.
*/
-package com.android.systemui.statusbar.notification.shared
+package com.android.systemui.statusbar.notification.emptyshade.shared
-import android.os.SystemProperties
-import com.android.systemui.Flags
+import android.app.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
-/** Helper for reading or using the minimalism prototype flag state. */
+/** Helper for reading or using the modes_ui_empty_shade flag state. */
@Suppress("NOTHING_TO_INLINE")
-object NotificationMinimalismPrototype {
- const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+object ModesEmptyShadeFix {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_MODES_UI_EMPTY_SHADE
/** A token used for dependency declaration */
val token: FlagToken
get() = FlagToken(FLAG_NAME, isEnabled)
- /** Is the heads-up cycling animation enabled */
+ /** Is the refactor enabled */
@JvmStatic
inline val isEnabled
- get() = Flags.notificationMinimalismPrototype()
-
- /**
- * The prototype will (by default) use a promoter to ensure that the top unseen notification is
- * not grouped, but this property read allows that behavior to be disabled.
- */
- val ungroupTopUnseen: Boolean
- get() =
- if (isUnexpectedlyInLegacyMode()) false
- else
- SystemProperties.getBoolean(
- "persist.notification_minimalism_prototype.ungroup_top_unseen",
- false
- )
+ get() = Flags.modesUiEmptyShade()
/**
* Called to ensure code is only run when the flag is enabled. This protects users from the
@@ -59,6 +46,14 @@
/**
* Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is not enabled to ensure that the refactor author catches issues in testing.
+ * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+ */
+ @JvmStatic
+ inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
* the flag is enabled to ensure that the refactor author catches issues in testing.
*/
@JvmStatic
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
index 850e944..73477da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/view/EmptyShadeView.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
@@ -29,26 +30,71 @@
import androidx.annotation.NonNull;
+import com.android.systemui.animation.LaunchableView;
+import com.android.systemui.animation.LaunchableViewDelegate;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
-public class EmptyShadeView extends StackScrollerDecorView {
+import kotlin.Unit;
+
+import java.util.Objects;
+
+public class EmptyShadeView extends StackScrollerDecorView implements LaunchableView {
private TextView mEmptyText;
private TextView mEmptyFooterText;
- private @StringRes int mText = R.string.empty_shade_text;
+ private @StringRes int mTextId = R.string.empty_shade_text;
+ private String mTextString;
- private @DrawableRes int mFooterIcon = R.drawable.ic_friction_lock_closed;
- private @StringRes int mFooterText = R.string.unlock_to_see_notif_text;
+ private @DrawableRes int mFooterIcon;
+ private @StringRes int mFooterText;
+ // This view is initially gone in the xml.
private @Visibility int mFooterVisibility = View.GONE;
private int mSize;
+ private LaunchableViewDelegate mLaunchableViewDelegate = new LaunchableViewDelegate(this,
+ visibility -> {
+ super.setVisibility(visibility);
+ return Unit.INSTANCE;
+ });
+
public EmptyShadeView(Context context, AttributeSet attrs) {
super(context, attrs);
mSize = getResources().getDimensionPixelSize(
R.dimen.notifications_unseen_footer_icon_size);
+ if (ModesEmptyShadeFix.isEnabled()) {
+ mTextString = getContext().getString(R.string.empty_shade_text);
+ } else {
+ // These will be set by the binder when appropriate if ModesEmptyShadeFix is on.
+ mFooterIcon = R.drawable.ic_friction_lock_closed;
+ mFooterText = R.string.unlock_to_see_notif_text;
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ mLaunchableViewDelegate.setVisibility(visibility);
+ }
+
+ @Override
+ public void setShouldBlockVisibilityChanges(boolean block) {
+ /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+ mLaunchableViewDelegate.setShouldBlockVisibilityChanges(block);
+ }
+
+ @Override
+ public void onActivityLaunchAnimationEnd() {
+ /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+ }
+
+ @Override
+ @NonNull
+ public Rect getPaddingForLaunchAnimation() {
+ /* check if */ ModesEmptyShadeFix.isUnexpectedlyInLegacyMode();
+ return new Rect();
}
@Override
@@ -56,7 +102,11 @@
super.onConfigurationChanged(newConfig);
mSize = getResources().getDimensionPixelSize(
R.dimen.notifications_unseen_footer_icon_size);
- mEmptyText.setText(mText);
+ if (ModesEmptyShadeFix.isEnabled()) {
+ mEmptyText.setText(mTextString);
+ } else {
+ mEmptyText.setText(mTextId);
+ }
mEmptyFooterText.setVisibility(mFooterVisibility);
setFooterText(mFooterText);
setFooterIcon(mFooterIcon);
@@ -72,25 +122,45 @@
return findViewById(R.id.no_notifications_footer);
}
+ /** Update view colors. */
public void setTextColors(@ColorInt int onSurface, @ColorInt int onSurfaceVariant) {
mEmptyText.setTextColor(onSurfaceVariant);
mEmptyFooterText.setTextColor(onSurface);
mEmptyFooterText.setCompoundDrawableTintList(ColorStateList.valueOf(onSurface));
}
+ /** Set the resource ID for the main text shown by the view. */
public void setText(@StringRes int text) {
- mText = text;
- mEmptyText.setText(mText);
+ ModesEmptyShadeFix.assertInLegacyMode();
+ mTextId = text;
+ mEmptyText.setText(mTextId);
}
+ /** Set the string for the main text shown by the view. */
+ public void setText(String text) {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || Objects.equals(mTextString, text)) {
+ return;
+ }
+ mTextString = text;
+ mEmptyText.setText(text);
+ }
+
+ /** Visibility for the footer (the additional icon+text shown below the main text). */
public void setFooterVisibility(@Visibility int visibility) {
+ if (ModesEmptyShadeFix.isEnabled() && mFooterVisibility == visibility) {
+ return; // nothing to change
+ }
mFooterVisibility = visibility;
setSecondaryVisible(/* visible = */ visibility == View.VISIBLE,
/* animate = */false,
/* onAnimationEnded = */ null);
}
+ /** Text resource ID for the footer (the additional icon+text shown below the main text). */
public void setFooterText(@StringRes int text) {
+ if (ModesEmptyShadeFix.isEnabled() && mFooterText == text) {
+ return; // nothing to change
+ }
mFooterText = text;
if (text != 0) {
mEmptyFooterText.setText(mFooterText);
@@ -99,7 +169,11 @@
}
}
+ /** Icon resource ID for the footer (the additional icon+text shown below the main text). */
public void setFooterIcon(@DrawableRes int icon) {
+ if (ModesEmptyShadeFix.isEnabled() && mFooterIcon == icon) {
+ return; // nothing to change
+ }
mFooterIcon = icon;
Drawable drawable;
if (icon == 0) {
@@ -111,18 +185,24 @@
mEmptyFooterText.setCompoundDrawablesRelative(drawable, null, null, null);
}
+ /** Get resource ID for main text. */
@StringRes
public int getTextResource() {
- return mText;
+ ModesEmptyShadeFix.assertInLegacyMode();
+ return mTextId;
}
+ /** Get resource ID for footer text. */
@StringRes
public int getFooterTextResource() {
+ ModesEmptyShadeFix.assertInLegacyMode();
return mFooterText;
}
+ /** Get resource ID for footer icon. */
@DrawableRes
public int getFooterIconResource() {
+ ModesEmptyShadeFix.assertInLegacyMode();
return mFooterIcon;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
new file mode 100644
index 0000000..102a11c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewbinder/EmptyShadeViewBinder.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder
+
+import android.view.View
+import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+object EmptyShadeViewBinder {
+ suspend fun bind(
+ view: EmptyShadeView,
+ viewModel: EmptyShadeViewModel,
+ launchNotificationSettings: View.OnClickListener,
+ launchNotificationHistory: View.OnClickListener,
+ ) = coroutineScope {
+ launch { viewModel.text.collect { view.setText(it) } }
+
+ launch {
+ viewModel.tappingShouldLaunchHistory.collect { shouldLaunchHistory ->
+ if (shouldLaunchHistory) {
+ view.setOnClickListener(launchNotificationHistory)
+ } else {
+ view.setOnClickListener(launchNotificationSettings)
+ }
+ }
+ }
+
+ launch { bindFooter(view, viewModel) }
+ }
+
+ private suspend fun bindFooter(view: EmptyShadeView, viewModel: EmptyShadeViewModel) =
+ coroutineScope {
+ // Bind the resource IDs
+ view.setFooterText(viewModel.footer.messageId)
+ view.setFooterIcon(viewModel.footer.iconId)
+
+ launch {
+ viewModel.footer.isVisible.collect { visible ->
+ view.setFooterVisibility(if (visible) View.VISIBLE else View.GONE)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.kt
new file mode 100644
index 0000000..d5417e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModel.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.statusbar.notification.emptyshade.ui.viewmodel
+
+import android.content.Context
+import android.icu.text.MessageFormat
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.modes.shared.ModesUi
+import com.android.systemui.res.R
+import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
+import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterMessageViewModel
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
+import com.android.systemui.util.kotlin.FlowDumperImpl
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.Locale
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/**
+ * ViewModel for the empty shade (aka the "No notifications" text shown when there are no
+ * notifications.
+ */
+class EmptyShadeViewModel
+@AssistedInject
+constructor(
+ private val context: Context,
+ zenModeInteractor: ZenModeInteractor,
+ seenNotificationsInteractor: SeenNotificationsInteractor,
+ notificationSettingsInteractor: NotificationSettingsInteractor,
+ dumpManager: DumpManager,
+) : FlowDumperImpl(dumpManager) {
+ val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting(
+ "areNotificationsHiddenInShade"
+ )
+ }
+ }
+
+ val hasFilteredOutSeenNotifications: StateFlow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ MutableStateFlow(false)
+ } else {
+ seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpValue(
+ "hasFilteredOutSeenNotifications"
+ )
+ }
+ }
+
+ val text: Flow<String> by lazy {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
+ flowOf(context.getString(R.string.empty_shade_text))
+ } else {
+ // Note: Flag modes_ui_empty_shade includes two pieces: refactoring the empty shade to
+ // recommended architecture, and making it so it reacts to changes for the new Modes.
+ // The former does not depend on the modes flags being on, but the latter does.
+ if (ModesUi.isEnabled) {
+ zenModeInteractor.modesHidingNotifications.map { modes ->
+ // Create a string that is either "No notifications" if no modes are filtering
+ // them
+ // out, or something like "Notifications paused by SomeMode" otherwise.
+ val msgFormat =
+ MessageFormat(
+ context.getString(R.string.modes_suppressing_shade_text),
+ Locale.getDefault(),
+ )
+ val count = modes.count()
+ val args: MutableMap<String, Any> = HashMap()
+ args["count"] = count
+ if (count >= 1) {
+ args["mode"] = modes[0].name
+ }
+ msgFormat.format(args)
+ }
+ } else {
+ areNotificationsHiddenInShade.map { areNotificationsHiddenInShade ->
+ if (areNotificationsHiddenInShade) {
+ context.getString(R.string.dnd_suppressing_shade_text)
+ } else {
+ context.getString(R.string.empty_shade_text)
+ }
+ }
+ }
+ }
+ }
+
+ val footer: FooterMessageViewModel by lazy {
+ ModesEmptyShadeFix.assertInNewMode()
+ FooterMessageViewModel(
+ messageId = R.string.unlock_to_see_notif_text,
+ iconId = R.drawable.ic_friction_lock_closed,
+ isVisible = hasFilteredOutSeenNotifications,
+ )
+ }
+
+ val tappingShouldLaunchHistory by lazy {
+ ModesEmptyShadeFix.assertInNewMode()
+ notificationSettingsInteractor.isNotificationHistoryEnabled
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): EmptyShadeViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index 291dc13..cd228e7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -72,14 +72,24 @@
}
/**
+ * See {@link #setVisible(boolean, boolean, Consumer)}.
+ */
+ public void setVisible(boolean visible, boolean animate) {
+ setVisible(visible, animate, null /* onAnimationEnded */);
+ }
+
+ /**
* Make this view visible. If {@code false} is passed, the view will fade out its content
* and set the view Visibility to GONE. If only the content should be changed,
* {@link #setContentVisibleAnimated(boolean)} can be used.
*
* @param visible True if the contents should be visible.
* @param animate True if we should fade to new visibility.
+ * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a
+ * parameter that represents whether the animation was cancelled.
*/
- public void setVisible(boolean visible, boolean animate) {
+ public void setVisible(boolean visible, boolean animate,
+ Consumer<Boolean> onAnimationEnded) {
if (mIsVisible != visible) {
mIsVisible = visible;
if (animate) {
@@ -90,10 +100,10 @@
} else {
setWillBeGone(true);
}
- setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
+ setContentVisible(visible, true /* animate */, onAnimationEnded);
} else {
setVisibility(visible ? VISIBLE : GONE);
- setContentVisible(visible, false /* animate */, null /* onAnimationEnded */);
+ setContentVisible(visible, false /* animate */, onAnimationEnded);
setWillBeGone(false);
notifyHeightChanged(false /* needsAnimation */);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
similarity index 89%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
index 06f3db5..70bb272 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalismPrototype.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationMinimalism.kt
@@ -17,23 +17,23 @@
package com.android.systemui.statusbar.notification.shared
import android.os.SystemProperties
-import com.android.systemui.Flags
+import com.android.server.notification.Flags
import com.android.systemui.flags.FlagToken
import com.android.systemui.flags.RefactorFlagUtils
/** Helper for reading or using the minimalism prototype flag state. */
@Suppress("NOTHING_TO_INLINE")
-object NotificationMinimalismPrototype {
- const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM_PROTOTYPE
+object NotificationMinimalism {
+ const val FLAG_NAME = Flags.FLAG_NOTIFICATION_MINIMALISM
/** A token used for dependency declaration */
val token: FlagToken
get() = FlagToken(FLAG_NAME, isEnabled)
- /** Is the heads-up cycling animation enabled */
+ /** Is the notification minimalism enabled */
@JvmStatic
inline val isEnabled
- get() = Flags.notificationMinimalismPrototype()
+ get() = Flags.notificationMinimalism()
/**
* The prototype will (by default) use a promoter to ensure that the top unseen notification is
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
index 1431b28..d246b04 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java
@@ -88,6 +88,8 @@
private ExpandableView mLastVisibleBackgroundChild;
private float mCurrentScrollVelocity;
private int mStatusBarState;
+ private boolean mShowingStackOnLockscreen;
+ private float mLockscreenStackFadeInProgress;
private float mExpandingVelocity;
private boolean mPanelTracking;
private boolean mExpansionChanging;
@@ -624,6 +626,26 @@
return mStatusBarState == StatusBarState.KEYGUARD;
}
+ public boolean isShowingStackOnLockscreen() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return false;
+ return mShowingStackOnLockscreen;
+ }
+
+ public void setShowingStackOnLockscreen(boolean showingStackOnLockscreen) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mShowingStackOnLockscreen = showingStackOnLockscreen;
+ }
+
+ public float getLockscreenStackFadeInProgress() {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return 0f;
+ return mLockscreenStackFadeInProgress;
+ }
+
+ public void setLockscreenStackFadeInProgress(float lockscreenStackFadeInProgress) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mLockscreenStackFadeInProgress = lockscreenStackFadeInProgress;
+ }
+
public void setStatusBarState(int statusBarState) {
if (mStatusBarState != StatusBarState.KEYGUARD) {
mIsFlingRequiredAfterLockScreenSwipeUp = false;
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 cd3516d..0a44a2b 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
@@ -104,6 +104,7 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix;
import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView;
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView;
@@ -686,7 +687,9 @@
protected void onFinishInflate() {
super.onFinishInflate();
- inflateEmptyShadeView();
+ if (!ModesEmptyShadeFix.isEnabled()) {
+ inflateEmptyShadeView();
+ }
if (!FooterViewRefactor.isEnabled()) {
inflateFooterView();
}
@@ -729,7 +732,9 @@
inflateFooterView();
updateFooter();
}
- inflateEmptyShadeView();
+ if (!ModesEmptyShadeFix.isEnabled()) {
+ inflateEmptyShadeView();
+ }
mSectionsManager.reinflateViews();
}
@@ -4835,6 +4840,8 @@
/** Trigger an update for the empty shade resources and visibility. */
public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade,
boolean hasFilteredOutSeenNotifications) {
+ ModesEmptyShadeFix.assertInLegacyMode();
+
mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled);
if (areNotificationsHiddenInShade) {
@@ -4853,6 +4860,8 @@
@StringRes int newTextRes,
@StringRes int newFooterTextRes,
@DrawableRes int newFooterIconRes) {
+ ModesEmptyShadeFix.assertInLegacyMode();
+
int oldTextRes = mEmptyShadeView.getTextResource();
if (oldTextRes != newTextRes) {
mEmptyShadeView.setText(newTextRes);
@@ -4874,6 +4883,9 @@
public boolean isEmptyShadeViewVisible() {
SceneContainerFlag.assertInLegacyMode();
+ if (mEmptyShadeView == null) {
+ return false;
+ }
return mEmptyShadeView.isVisible();
}
@@ -5330,6 +5342,19 @@
updateDismissBehavior();
}
+ @Override
+ public void setShowingStackOnLockscreen(boolean showingStackOnLockscreen) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mAmbientState.setShowingStackOnLockscreen(showingStackOnLockscreen);
+ }
+
+ @Override
+ public void setAlphaForLockscreenFadeIn(float alphaForLockscreenFadeIn) {
+ if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
+ mAmbientState.setLockscreenStackFadeInProgress(alphaForLockscreenFadeIn);
+ requestChildrenUpdate();
+ }
+
void setUpcomingStatusBarState(int upcomingStatusBarState) {
FooterViewRefactor.assertInLegacyMode();
mUpcomingStatusBarState = upcomingStatusBarState;
@@ -5361,7 +5386,7 @@
public float getOpeningHeight() {
SceneContainerFlag.assertInLegacyMode();
- if (mEmptyShadeView.getVisibility() == GONE) {
+ if (mEmptyShadeView == null || mEmptyShadeView.getVisibility() == GONE) {
return getMinExpansionHeight();
} else {
return FooterViewRefactor.isEnabled() ? getAppearEndPosition()
@@ -5710,6 +5735,8 @@
}
private void inflateEmptyShadeView() {
+ ModesEmptyShadeFix.assertInLegacyMode();
+
EmptyShadeView oldView = mEmptyShadeView;
EmptyShadeView view = (EmptyShadeView) LayoutInflater.from(mContext).inflate(
R.layout.status_bar_no_notifications, this, false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index dad6894..9c5fecf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -380,7 +380,7 @@
new StatusBarStateController.StateListener() {
@Override
public void onStatePreChange(int oldState, int newState) {
- if (oldState == StatusBarState.SHADE_LOCKED
+ if (!SceneContainerFlag.isEnabled() && oldState == StatusBarState.SHADE_LOCKED
&& newState == KEYGUARD) {
mView.requestAnimateEverything();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
index 06222fd..3bc5495 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt
@@ -29,7 +29,7 @@
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.Compile
import com.android.systemui.util.children
@@ -74,7 +74,7 @@
/** Whether we allow keyguard to show less important notifications above the shelf. */
private val limitLockScreenToOneImportant
- get() = NotificationMinimalismPrototype.isEnabled
+ get() = NotificationMinimalism.isEnabled
/** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */
private var dividerHeight by notNull<Float>()
@@ -406,7 +406,7 @@
fun updateResources() {
maxKeyguardNotifications =
infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count))
- maxNotificationsExcludesMedia = NotificationMinimalismPrototype.isEnabled
+ maxNotificationsExcludesMedia = NotificationMinimalism.isEnabled
dividerHeight =
max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 9c0fd0e..e0b0ccd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -148,12 +148,18 @@
if (isHunGoingToShade) {
// Keep 100% opacity for heads up notification going to shade.
viewState.setAlpha(1f);
- } else if (ambientState.isOnKeyguard()) {
+ } else if ((!SceneContainerFlag.isEnabled() && ambientState.isOnKeyguard())
+ || ambientState.isShowingStackOnLockscreen()) {
// Adjust alpha for wakeup to lockscreen.
if (view.isHeadsUpState()) {
// Pulsing HUN should be visible on AOD and stay visible during
// AOD=>lockscreen transition
viewState.setAlpha(1f - ambientState.getHideAmount());
+ } else if (SceneContainerFlag.isEnabled()) {
+ // Take into account scene container-specific Lockscreen fade-in progress
+ float fadeAlpha = ambientState.getLockscreenStackFadeInProgress();
+ float dozeAlpha = 1f - ambientState.getDozeAmount();
+ viewState.setAlpha(Math.min(dozeAlpha, fadeAlpha));
} else {
// Normal notifications are hidden on AOD and should fade in during
// AOD=>lockscreen transition
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
index f6722a4..c0f1a56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationPlaceholderRepository.kt
@@ -31,6 +31,9 @@
/** The alpha of the shade in order to show brightness. */
val alphaForBrightnessMirror = MutableStateFlow(1f)
+ /** The alpha of the Notification Stack for lockscreen fade-in */
+ val alphaForLockscreenFadeIn = MutableStateFlow(0f)
+
/**
* The bounds of the notification shade scrim / container in the current scene.
*
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
index 756cd87..32e092b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt
@@ -56,10 +56,9 @@
/** The rounding of the notification stack. */
val shadeScrimRounding: Flow<ShadeScrimRounding> =
- combine(
- shadeInteractor.shadeMode,
- isExpandingFromHeadsUp,
- ) { shadeMode, isExpandingFromHeadsUp ->
+ combine(shadeInteractor.shadeMode, isExpandingFromHeadsUp) {
+ shadeMode,
+ isExpandingFromHeadsUp ->
ShadeScrimRounding(
isTopRounded = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp),
isBottomRounded = shadeMode != ShadeMode.Single,
@@ -71,6 +70,10 @@
val alphaForBrightnessMirror: StateFlow<Float> =
placeholderRepository.alphaForBrightnessMirror.asStateFlow()
+ /** The alpha of the Notification Stack for lockscreen fade-in */
+ val alphaForLockscreenFadeIn: StateFlow<Float> =
+ placeholderRepository.alphaForLockscreenFadeIn.asStateFlow()
+
/** The height of the keyguard's available space bounds */
val constrainedAvailableSpace: StateFlow<Int> =
placeholderRepository.constrainedAvailableSpace.asStateFlow()
@@ -99,7 +102,7 @@
val shouldCloseGuts: Flow<Boolean> =
combine(
sceneInteractor.isSceneContainerUserInputOngoing,
- viewHeightRepository.isCurrentGestureInGuts
+ viewHeightRepository.isCurrentGestureInGuts,
) { isUserInputOngoing, isCurrentGestureInGuts ->
isUserInputOngoing && !isCurrentGestureInGuts
}
@@ -109,6 +112,11 @@
placeholderRepository.alphaForBrightnessMirror.value = alpha
}
+ /** Sets the alpha to apply to the NSSL for fade-in on lockscreen */
+ fun setAlphaForLockscreenFadeIn(alpha: Float) {
+ placeholderRepository.alphaForLockscreenFadeIn.value = alpha
+ }
+
/** Sets the position of the notification stack in the current scene. */
fun setShadeScrimBounds(bounds: ShadeScrimBounds?) {
check(bounds == null || bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
index 41c0293..0113e36 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt
@@ -89,6 +89,12 @@
/** sets the current QS expand fraction */
fun setQsExpandFraction(expandFraction: Float)
+ /** set whether we are idle on the lockscreen scene */
+ fun setShowingStackOnLockscreen(showingStackOnLockscreen: Boolean)
+
+ /** set the alpha from 0-1f of stack fade-in on lockscreen */
+ fun setAlphaForLockscreenFadeIn(alphaForLockscreenFadeIn: Float)
+
/** Sets whether the view is displayed in doze mode. */
fun setDozing(dozing: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index dc9615c..3dad326 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.stack.ui.viewbinder
import android.view.LayoutInflater
+import android.view.View
import androidx.lifecycle.lifecycleScope
import com.android.app.tracing.TraceUtils.traceAsync
import com.android.internal.logging.MetricsLogger
@@ -25,6 +26,7 @@
import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -32,6 +34,10 @@
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController
import com.android.systemui.statusbar.notification.dagger.SilentHeader
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewbinder.EmptyShadeViewBinder
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
@@ -49,6 +55,7 @@
import com.android.systemui.util.kotlin.awaitCancellationThenDispose
import com.android.systemui.util.kotlin.getOrNull
import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.stopAnimating
import com.android.systemui.util.ui.value
import java.util.Optional
import javax.inject.Inject
@@ -84,7 +91,7 @@
fun bindWhileAttached(
view: NotificationStackScrollLayout,
- viewController: NotificationStackScrollLayoutController
+ viewController: NotificationStackScrollLayoutController,
) {
val shelf =
LayoutInflater.from(view.context)
@@ -103,7 +110,13 @@
val hasNonClearableSilentNotifications: StateFlow<Boolean> =
viewModel.hasNonClearableSilentNotifications.stateIn(this)
launch { reinflateAndBindFooter(view, hasNonClearableSilentNotifications) }
- launch { bindEmptyShade(view) }
+ launch {
+ if (ModesEmptyShadeFix.isEnabled) {
+ reinflateAndBindEmptyShade(view)
+ } else {
+ bindEmptyShadeLegacy(viewModel.emptyShadeViewFactory.create(), view)
+ }
+ }
launch {
bindSilentHeaderClickListener(view, hasNonClearableSilentNotifications)
}
@@ -121,17 +134,12 @@
}
private suspend fun bindShelf(shelf: NotificationShelf) {
- NotificationShelfViewBinder.bind(
- shelf,
- viewModel.shelf,
- falsingManager,
- nicBinder,
- )
+ NotificationShelfViewBinder.bind(shelf, viewModel.shelf, falsingManager, nicBinder)
}
private suspend fun reinflateAndBindFooter(
parentView: NotificationStackScrollLayout,
- hasNonClearableSilentNotifications: StateFlow<Boolean>
+ hasNonClearableSilentNotifications: StateFlow<Boolean>,
) {
viewModel.footer.getOrNull()?.let { footerViewModel ->
// The footer needs to be re-inflated every time the theme or the font size changes.
@@ -149,7 +157,7 @@
footerView,
footerViewModel,
parentView,
- hasNonClearableSilentNotifications
+ hasNonClearableSilentNotifications,
)
}
}
@@ -163,13 +171,13 @@
footerView: FooterView,
footerViewModel: FooterViewModel,
parentView: NotificationStackScrollLayout,
- hasNonClearableSilentNotifications: StateFlow<Boolean>
+ hasNonClearableSilentNotifications: StateFlow<Boolean>,
): Unit = coroutineScope {
val disposableHandle =
FooterViewBinder.bindWhileAttached(
footerView,
footerViewModel,
- clearAllNotifications = {
+ {
clearAllNotifications(
parentView,
// Hide the silent section header (if present) if there will be
@@ -177,16 +185,8 @@
hideSilentSection = !hasNonClearableSilentNotifications.value,
)
},
- launchNotificationSettings = { view ->
- notificationActivityStarter
- .get()
- .startHistoryIntent(view, /* showHistory= */ false)
- },
- launchNotificationHistory = { view ->
- notificationActivityStarter
- .get()
- .startHistoryIntent(view, /* showHistory= */ true)
- },
+ launchNotificationSettings,
+ launchNotificationHistory,
)
if (SceneContainerFlag.isEnabled) {
launch {
@@ -194,7 +194,9 @@
footerView.setVisible(
/* visible = */ animatedVisibility.value,
/* animate = */ animatedVisibility.isAnimating,
- )
+ ) {
+ animatedVisibility.stopAnimating()
+ }
}
}
} else {
@@ -211,20 +213,71 @@
disposableHandle.awaitCancellationThenDispose()
}
- private suspend fun bindEmptyShade(parentView: NotificationStackScrollLayout) {
+ private val launchNotificationSettings: (View) -> Unit = { view: View ->
+ notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ false)
+ }
+
+ private val launchNotificationHistory: (View) -> Unit = { view ->
+ notificationActivityStarter.get().startHistoryIntent(view, /* showHistory= */ true)
+ }
+
+ private suspend fun reinflateAndBindEmptyShade(parentView: NotificationStackScrollLayout) {
+ ModesEmptyShadeFix.assertInNewMode()
+ // The empty shade needs to be re-inflated every time the theme or the font size
+ // changes.
+ configuration
+ .inflateLayout<EmptyShadeView>(
+ R.layout.status_bar_no_notifications,
+ parentView,
+ attachToRoot = false,
+ )
+ .flowOn(backgroundDispatcher)
+ .collectLatest { emptyShadeView: EmptyShadeView ->
+ traceAsync("bind EmptyShadeView") {
+ parentView.setEmptyShadeView(emptyShadeView)
+ bindEmptyShade(emptyShadeView, viewModel.emptyShadeViewFactory.create())
+ }
+ }
+ }
+
+ private suspend fun bindEmptyShadeLegacy(
+ emptyShadeViewModel: EmptyShadeViewModel,
+ parentView: NotificationStackScrollLayout,
+ ) {
+ ModesEmptyShadeFix.assertInLegacyMode()
combine(
viewModel.shouldShowEmptyShadeView,
- viewModel.areNotificationsHiddenInShade,
- viewModel.hasFilteredOutSeenNotifications,
- ::Triple
+ emptyShadeViewModel.areNotificationsHiddenInShade,
+ emptyShadeViewModel.hasFilteredOutSeenNotifications,
+ ::Triple,
)
.collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
- parentView.updateEmptyShadeView(
- shouldShow,
- areNotifsHidden,
- hasFilteredNotifs,
+ parentView.updateEmptyShadeView(shouldShow, areNotifsHidden, hasFilteredNotifs)
+ }
+ }
+
+ private suspend fun bindEmptyShade(
+ emptyShadeView: EmptyShadeView,
+ emptyShadeViewModel: EmptyShadeViewModel,
+ ): Unit = coroutineScope {
+ ModesEmptyShadeFix.assertInNewMode()
+ launch {
+ emptyShadeView.repeatWhenAttachedToWindow {
+ EmptyShadeViewBinder.bind(
+ emptyShadeView,
+ emptyShadeViewModel,
+ launchNotificationSettings,
+ launchNotificationHistory,
)
}
+ }
+ launch {
+ viewModel.shouldShowEmptyShadeViewAnimated.collect { shouldShow ->
+ emptyShadeView.setVisible(shouldShow.value, shouldShow.isAnimating) {
+ shouldShow.stopAnimating()
+ }
+ }
+ }
}
private suspend fun bindSilentHeaderClickListener(
@@ -261,7 +314,7 @@
private fun clearSilentNotifications(
view: NotificationStackScrollLayout,
closeShade: Boolean,
- hideSilentSection: Boolean
+ hideSilentSection: Boolean,
) {
view.clearSilentNotifications(closeShade, hideSilentSection)
}
@@ -270,11 +323,7 @@
if (NotificationsLiveDataStoreRefactor.isEnabled) {
viewModel.logger.getOrNull()?.let { viewModel ->
loggerOptional.getOrNull()?.let { logger ->
- NotificationStatsLoggerBinder.bindLogger(
- view,
- logger,
- viewModel,
- )
+ NotificationStatsLoggerBinder.bindLogger(view, logger, viewModel)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index 2e37dea..99ff678 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -88,6 +88,14 @@
viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) }
}
launch { viewModel.qsExpandFraction.collect { view.setQsExpandFraction(it) } }
+ launch {
+ viewModel.isShowingStackOnLockscreen.collect {
+ view.setShowingStackOnLockscreen(it)
+ }
+ }
+ launch {
+ viewModel.alphaForLockscreenFadeIn.collect { view.setAlphaForLockscreenFadeIn(it) }
+ }
launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } }
launch {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 4e2a46d..935e2a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -23,14 +23,14 @@
import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.EmptyShadeViewModel
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor
import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor
-import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import com.android.systemui.util.kotlin.FlowDumperImpl
import com.android.systemui.util.kotlin.combine
import com.android.systemui.util.kotlin.sample
@@ -48,22 +48,24 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-/** ViewModel for the list of notifications. */
+/**
+ * ViewModel for the list of notifications, including child elements like the Clear all/Manage
+ * button at the bottom (the footer) and the "No notifications" text (the empty shade).
+ */
class NotificationListViewModel
@Inject
constructor(
val shelf: NotificationShelfViewModel,
val hideListViewModel: HideListViewModel,
val footer: Optional<FooterViewModel>,
+ val emptyShadeViewFactory: EmptyShadeViewModel.Factory,
val logger: Optional<NotificationLoggerViewModel>,
activeNotificationsInteractor: ActiveNotificationsInteractor,
notificationStackInteractor: NotificationStackInteractor,
private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
remoteInputInteractor: RemoteInputInteractor,
- seenNotificationsInteractor: SeenNotificationsInteractor,
shadeInteractor: ShadeInteractor,
userSetupInteractor: UserSetupInteractor,
- zenModeInteractor: ZenModeInteractor,
@Background bgDispatcher: CoroutineDispatcher,
dumpManager: DumpManager,
) : FlowDumperImpl(dumpManager) {
@@ -90,6 +92,7 @@
}
val shouldShowEmptyShadeView: Flow<Boolean> by lazy {
+ ModesEmptyShadeFix.assertInLegacyMode()
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
flowOf(false)
} else {
@@ -114,6 +117,45 @@
}
}
+ val shouldShowEmptyShadeViewAnimated: Flow<AnimatedValue<Boolean>> by lazy {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode()) {
+ flowOf(AnimatedValue.NotAnimating(false))
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ shadeInteractor.isQsFullscreen,
+ notificationStackInteractor.isShowingOnLockscreen,
+ ) { hasNotifications, isQsFullScreen, isShowingOnLockscreen ->
+ when {
+ hasNotifications -> false
+ isQsFullScreen -> false
+ // Do not show the empty shade if the lockscreen is visible (including AOD
+ // b/228790482 and bouncer b/267060171), except if the shade is opened on
+ // top.
+ isShowingOnLockscreen -> false
+ else -> true
+ }
+ }
+ .distinctUntilChanged()
+ .sample(
+ // TODO(b/322167853): This check is currently duplicated in FooterViewModel
+ // but instead it should be a field in ShadeAnimationInteractor.
+ combine(
+ shadeInteractor.isShadeFullyExpanded,
+ shadeInteractor.isShadeTouchable,
+ ::Pair,
+ )
+ .onStart { emit(Pair(false, false)) }
+ ) { visible, (isShadeFullyExpanded, animationsEnabled) ->
+ val shouldAnimate = isShadeFullyExpanded && animationsEnabled
+ AnimatableEvent(visible, shouldAnimate)
+ }
+ .toAnimatedValueFlow()
+ .dumpWhileCollecting("shouldShowEmptyShadeViewAnimated")
+ .flowOn(bgDispatcher)
+ }
+ }
+
/**
* Whether the footer should not be visible for the user, even if it's present in the list (as
* per [shouldIncludeFooterView] below).
@@ -154,7 +196,7 @@
userSetupInteractor.isUserSetUp,
notificationStackInteractor.isShowingOnLockscreen,
shadeInteractor.isQsFullscreen,
- remoteInputInteractor.isRemoteInputActive
+ remoteInputInteractor.isRemoteInputActive,
) {
hasNotifications,
isUserSetUp,
@@ -193,7 +235,7 @@
combine(
shadeInteractor.isShadeFullyExpanded,
shadeInteractor.isShadeTouchable,
- ::Pair
+ ::Pair,
)
.onStart { emit(Pair(false, false)) }
) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
@@ -263,7 +305,7 @@
combine(
shadeInteractor.isShadeFullyExpanded,
shadeInteractor.isShadeTouchable,
- ::Pair
+ ::Pair,
)
.onStart { emit(Pair(false, false)) }
) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) ->
@@ -283,29 +325,7 @@
enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) {
DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false),
DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true),
- APPEAR_WITH_ANIMATION(visible = true, canAnimate = true)
- }
-
- // TODO(b/308591475): This should be tracked separately by the empty shade.
- val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
- if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
- flowOf(false)
- } else {
- zenModeInteractor.areNotificationsHiddenInShade.dumpWhileCollecting(
- "areNotificationsHiddenInShade"
- )
- }
- }
-
- // TODO(b/308591475): This should be tracked separately by the empty shade.
- val hasFilteredOutSeenNotifications: Flow<Boolean> by lazy {
- if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
- flowOf(false)
- } else {
- seenNotificationsInteractor.hasFilteredOutSeenNotifications.dumpWhileCollecting(
- "hasFilteredOutSeenNotifications"
- )
- }
+ APPEAR_WITH_ANIMATION(visible = true, canAnimate = true),
}
val hasClearableAlertingNotifications: Flow<Boolean> by lazy {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt
new file mode 100644
index 0000000..84aa997
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLockscreenScrimViewModel.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.util.kotlin.ActivatableFlowDumper
+import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class NotificationLockscreenScrimViewModel
+@AssistedInject
+constructor(
+ dumpManager: DumpManager,
+ shadeInteractor: ShadeInteractor,
+ private val stackAppearanceInteractor: NotificationStackAppearanceInteractor,
+) :
+ ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"),
+ ExclusiveActivatable() {
+
+ val shadeMode = shadeInteractor.shadeMode
+
+ /** Sets the alpha to apply to the NSSL for fade-in on lockscreen */
+ fun setAlphaForLockscreenFadeIn(alpha: Float) {
+ stackAppearanceInteractor.setAlphaForLockscreenFadeIn(alpha)
+ }
+
+ override suspend fun onActivated(): Nothing {
+ activateFlowDumper()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): NotificationLockscreenScrimViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 5b2e02d..cd9c07e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -82,8 +82,13 @@
private fun fullyExpandedDuringSceneChange(change: ChangeScene): Boolean {
// The lockscreen stack is visible during all transitions away from the lockscreen, so keep
// the stack expanded until those transitions finish.
- return (expandedInScene(change.fromScene) && expandedInScene(change.toScene)) ||
- change.isBetween({ it == Scenes.Lockscreen }, { true })
+ return if (change.isFrom({ it == Scenes.Lockscreen }, to = { true })) {
+ true
+ } else if (change.isFrom({ it == Scenes.Shade }, to = { it == Scenes.Lockscreen })) {
+ false
+ } else {
+ (expandedInScene(change.fromScene) && expandedInScene(change.toScene))
+ }
}
private fun expandFractionDuringSceneChange(
@@ -93,7 +98,10 @@
): Float {
return if (fullyExpandedDuringSceneChange(change)) {
1f
- } else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade })) {
+ } else if (
+ change.isBetween({ it == Scenes.Gone }, { it == Scenes.Shade }) ||
+ change.isFrom({ it == Scenes.Shade }, to = { it == Scenes.Lockscreen })
+ ) {
shadeExpansion
} else if (change.isBetween({ it == Scenes.Gone }, { it == Scenes.QuickSettings })) {
// during QS expansion, increase fraction at same rate as scrim alpha,
@@ -178,6 +186,18 @@
.distinctUntilChanged()
.dumpWhileCollecting("shouldResetStackTop")
+ /** Whether the Notification Stack is visibly on the lockscreen scene. */
+ val isShowingStackOnLockscreen: Flow<Boolean> =
+ sceneInteractor.transitionState
+ .mapNotNull { state ->
+ state.isIdle(Scenes.Lockscreen) ||
+ state.isTransitioning(from = Scenes.Lockscreen, to = Scenes.Shade)
+ }
+ .distinctUntilChanged()
+
+ /** The alpha of the Notification Stack for lockscreen fade-in */
+ val alphaForLockscreenFadeIn = stackAppearanceInteractor.alphaForLockscreenFadeIn
+
private operator fun SceneKey.contains(scene: SceneKey) =
sceneInteractor.isSceneInFamily(scene, this)
@@ -298,3 +318,6 @@
private fun ChangeScene.isBetween(a: (SceneKey) -> Boolean, b: (SceneKey) -> Boolean): Boolean =
(a(fromScene) && b(toScene)) || (b(fromScene) && a(toScene))
+
+private fun ChangeScene.isFrom(from: (SceneKey) -> Boolean, to: (SceneKey) -> Boolean): Boolean =
+ from(fromScene) && to(toScene)
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 caf09a3..674cbb7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/AvalancheController.kt
@@ -46,8 +46,6 @@
private val tag = "AvalancheController"
private val debug = Compile.IS_DEBUG && Log.isLoggable(tag, Log.DEBUG)
- var baseEntryMapStr : () -> String = { "baseEntryMapStr not initialized" }
-
var enableAtRuntime = true
set(value) {
if (!value) {
@@ -118,43 +116,32 @@
val key = getKey(entry)
if (runnable == null) {
- headsUpManagerLogger.logAvalancheUpdate(
- caller, isEnabled, key,
- "Runnable NULL, stop. ${getStateStr()}"
- )
+ headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Runnable NULL, stop")
return
}
if (!isEnabled) {
- headsUpManagerLogger.logAvalancheUpdate(
- caller, isEnabled, key,
- "NOT ENABLED, run runnable. ${getStateStr()}"
- )
+ headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key,
+ "NOT ENABLED, run runnable")
runnable.run()
return
}
if (entry == null) {
- headsUpManagerLogger.logAvalancheUpdate(
- caller, isEnabled, key,
- "Entry NULL, stop. ${getStateStr()}"
- )
+ headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, "Entry NULL, stop")
return
}
if (debug) {
debugRunnableLabelMap[runnable] = caller
}
- var stateAfter = ""
+ var outcome = ""
if (isShowing(entry)) {
+ outcome = "update showing"
runnable.run()
- stateAfter = "update showing"
-
} else if (entry in nextMap) {
+ outcome = "update next"
nextMap[entry]?.add(runnable)
- stateAfter = "update next"
-
} else if (headsUpEntryShowing == null) {
+ outcome = "show now"
showNow(entry, arrayListOf(runnable))
- stateAfter = "show now"
-
} else {
// Clean up invalid state when entry is in list but not map and vice versa
if (entry in nextMap) nextMap.remove(entry)
@@ -175,8 +162,8 @@
)
}
}
- stateAfter += getStateStr()
- headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled = true, key, stateAfter)
+ outcome += getStateStr()
+ headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, outcome)
}
@VisibleForTesting
@@ -194,40 +181,32 @@
val key = getKey(entry)
if (runnable == null) {
- headsUpManagerLogger.logAvalancheDelete(
- caller, isEnabled, key,
- "Runnable NULL, stop. ${getStateStr()}"
- )
+ headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key, "Runnable NULL, stop")
return
}
if (!isEnabled) {
+ headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
+ "NOT ENABLED, run runnable")
runnable.run()
- headsUpManagerLogger.logAvalancheDelete(
- caller, isEnabled = false, key,
- "NOT ENABLED, run runnable. ${getStateStr()}"
- )
return
}
if (entry == null) {
+ headsUpManagerLogger.logAvalancheDelete(caller, isEnabled, key,
+ "Entry NULL, run runnable")
runnable.run()
- headsUpManagerLogger.logAvalancheDelete(
- caller, isEnabled = true, key,
- "Entry NULL, run runnable. ${getStateStr()}"
- )
return
}
- val stateAfter: String
+ val outcome: String
if (entry in nextMap) {
+ outcome = "remove from next"
if (entry in nextMap) nextMap.remove(entry)
if (entry in nextList) nextList.remove(entry)
uiEventLogger.log(ThrottleEvent.AVALANCHE_THROTTLING_HUN_REMOVED)
- stateAfter = "remove from next. ${getStateStr()}"
-
} else if (entry in debugDropSet) {
+ outcome = "remove from dropset"
debugDropSet.remove(entry)
- stateAfter = "remove from dropset. ${getStateStr()}"
-
} else if (isShowing(entry)) {
+ outcome = "remove showing"
previousHunKey = getKey(headsUpEntryShowing)
// Show the next HUN before removing this one, so that we don't tell listeners
// onHeadsUpPinnedModeChanged, which causes
@@ -235,13 +214,11 @@
// HUN is animating out, resulting in a flicker.
showNext()
runnable.run()
- stateAfter = "remove showing. ${getStateStr()}"
-
} else {
+ outcome = "run runnable for untracked shown"
runnable.run()
- stateAfter = "run runnable for untracked shown HUN. ${getStateStr()}"
}
- headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), stateAfter)
+ headsUpManagerLogger.logAvalancheDelete(caller, isEnabled(), getKey(entry), outcome)
}
/**
@@ -423,14 +400,12 @@
}
private fun getStateStr(): String {
- return "\nAvalancheController:" +
+ return "\navalanche state:" +
"\n\tshowing: [${getKey(headsUpEntryShowing)}]" +
"\n\tprevious: [$previousHunKey]" +
"\n\tnext list: $nextListStr" +
"\n\tnext map: $nextMapStr" +
- "\n\tdropped: $dropSetStr" +
- "\nBHUM.mHeadsUpEntryMap: " +
- baseEntryMapStr()
+ "\n\tdropped: $dropSetStr"
}
private val dropSetStr: String
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 30524a5..f37393a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -116,7 +116,6 @@
mAccessibilityMgr = accessibilityManagerWrapper;
mUiEventLogger = uiEventLogger;
mAvalancheController = avalancheController;
- mAvalancheController.setBaseEntryMapStr(this::getEntryMapStr);
Resources resources = context.getResources();
mMinimumDisplayTime = NotificationThrottleHun.isEnabled()
? 500 : resources.getInteger(R.integer.heads_up_notification_minimum_time);
@@ -590,18 +589,6 @@
dumpInternal(pw, args);
}
- private String getEntryMapStr() {
- if (mHeadsUpEntryMap.isEmpty()) {
- return "EMPTY";
- }
- StringBuilder entryMapStr = new StringBuilder();
- for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) {
- entryMapStr.append("\n\t").append(
- entry.mEntry == null ? "null" : entry.mEntry.getKey());
- }
- return entryMapStr.toString();
- }
-
protected void dumpInternal(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.print(" mTouchAcceptanceDelay="); pw.println(mTouchAcceptanceDelay);
pw.print(" mSnoozeLengthMs="); pw.println(mSnoozeLengthMs);
@@ -1005,6 +992,7 @@
* Clear any pending removal runnables.
*/
public void cancelAutoRemovalCallbacks(@Nullable String reason) {
+ mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
Runnable runnable = () -> {
final boolean removed = cancelAutoRemovalCallbackInternal();
@@ -1013,7 +1001,6 @@
}
};
if (mEntry != null && isHeadsUpEntry(mEntry.getKey())) {
- mLogger.logAutoRemoveCancelRequest(this.mEntry, reason);
mAvalancheController.update(this, runnable, reason + " cancelAutoRemovalCallbacks");
} else {
// Just removed
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
index 41112cb..600270c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerLogger.kt
@@ -52,7 +52,7 @@
caller: String,
isEnabled: Boolean,
notifEntryKey: String,
- stateAfter: String
+ outcome: String
) {
buffer.log(
TAG,
@@ -60,7 +60,7 @@
{
str1 = caller
str2 = notifEntryKey
- str3 = stateAfter
+ str3 = outcome
bool1 = isEnabled
},
{ "$str1\n\t=> AC[isEnabled:$bool1] update: $str2\n\t=> $str3" }
@@ -71,7 +71,7 @@
caller: String,
isEnabled: Boolean,
notifEntryKey: String,
- stateAfter: String
+ outcome: String
) {
buffer.log(
TAG,
@@ -79,7 +79,7 @@
{
str1 = caller
str2 = notifEntryKey
- str3 = stateAfter
+ str3 = outcome
bool1 = isEnabled
},
{ "$str1\n\t=> AC[isEnabled:$bool1] delete: $str2\n\t=> $str3" }
@@ -136,7 +136,7 @@
str1 = entry.logKey
str2 = reason ?: "unknown"
},
- { "$str2 => request: cancelAutoRemovalCallbacks: $str1" }
+ { "request: cancel auto remove of $str1 reason: $str2" }
)
}
@@ -148,7 +148,7 @@
str1 = entry.logKey
str2 = reason ?: "unknown"
},
- { "$str2 => cancel auto remove: $str1" }
+ { "cancel auto remove of $str1 reason: $str2" }
)
}
@@ -161,7 +161,7 @@
str2 = reason
bool1 = isWaiting
},
- { "request: $str2 => removeEntry: $str1 isWaiting: $isWaiting" }
+ { "request: $str2 => remove entry $str1 isWaiting: $isWaiting" }
)
}
@@ -174,7 +174,7 @@
str2 = reason
bool1 = isWaiting
},
- { "$str2 => removeEntry: $str1 isWaiting: $isWaiting" }
+ { "$str2 => remove entry $str1 isWaiting: $isWaiting" }
)
}
@@ -216,12 +216,12 @@
str1 = logKey(key)
str2 = reason
},
- { "remove notif $str1 when headsUpEntry is null, reason: $str2" }
+ { "remove notification $str1 when headsUpEntry is null, reason: $str2" }
)
}
fun logNotificationActuallyRemoved(entry: NotificationEntry) {
- buffer.log(TAG, INFO, { str1 = entry.logKey }, { "removed: $str1 " })
+ buffer.log(TAG, INFO, { str1 = entry.logKey }, { "notification removed $str1 " })
}
fun logUpdateNotificationRequest(key: String, alert: Boolean, hasEntry: Boolean) {
@@ -233,7 +233,7 @@
bool1 = alert
bool2 = hasEntry
},
- { "request: update notif $str1 alert: $bool1 hasEntry: $bool2" }
+ { "request: update notification $str1 alert: $bool1 hasEntry: $bool2" }
)
}
@@ -246,7 +246,7 @@
bool1 = alert
bool2 = hasEntry
},
- { "update notif $str1 alert: $bool1 hasEntry: $bool2" }
+ { "update notification $str1 alert: $bool1 hasEntry: $bool2" }
)
}
@@ -281,7 +281,7 @@
bool1 = isPinned
str2 = reason
},
- { "$str2 => setEntryPinned[$bool1]: $str1" }
+ { "$str2 => set entry pinned $str1 pinned: $bool1" }
)
}
@@ -290,7 +290,7 @@
TAG,
INFO,
{ bool1 = hasPinnedNotification },
- { "hasPinnedNotification[$bool1]" }
+ { "has pinned notification changed to $bool1" }
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
index ba45942..daba109 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -20,6 +20,7 @@
import android.provider.Settings
import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
+import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
import android.util.Log
import androidx.concurrent.futures.await
import com.android.settingslib.notification.data.repository.ZenModeRepository
@@ -29,6 +30,7 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.modes.shared.ModesUi
import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
+import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
@@ -39,6 +41,7 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -54,8 +57,8 @@
private val notificationSettingsRepository: NotificationSettingsRepository,
@Background private val bgDispatcher: CoroutineDispatcher,
private val iconLoader: ZenIconLoader,
- private val deviceProvisioningRepository: DeviceProvisioningRepository,
- private val userSetupRepository: UserSetupRepository,
+ deviceProvisioningRepository: DeviceProvisioningRepository,
+ userSetupRepository: UserSetupRepository,
) {
val isZenAvailable: Flow<Boolean> =
combine(
@@ -126,6 +129,25 @@
val mainActiveMode: Flow<ZenModeInfo?> =
activeModes.map { a -> a.mainMode }.distinctUntilChanged()
+ val modesHidingNotifications: Flow<List<ZenMode>> by lazy {
+ if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || !ModesUi.isEnabled) {
+ flowOf(listOf())
+ } else {
+ modes
+ .map { modes ->
+ modes.filter { mode ->
+ mode.isActive &&
+ !mode.policy.isVisualEffectAllowed(
+ /* effect = */ VISUAL_EFFECT_NOTIFICATION_LIST,
+ /* defaultVal = */ true,
+ )
+ }
+ }
+ .flowOn(bgDispatcher)
+ .distinctUntilChanged()
+ }
+ }
+
suspend fun getModeIcon(mode: ZenMode): ZenIcon {
return iconLoader.getIcon(context, mode).await()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
index 71afa62..5cc6454 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -62,6 +62,7 @@
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.sceneContainerGestureFilterFactory
import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
@@ -70,6 +71,7 @@
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.testKosmos
import kotlin.time.Duration.Companion.seconds
@@ -133,6 +135,8 @@
shadeInteractor = kosmos.shadeInteractor,
splitEdgeDetector = kosmos.splitEdgeDetector,
logger = kosmos.sceneLogger,
+ gestureFilterFactory = kosmos.sceneContainerGestureFilterFactory,
+ displayId = kosmos.displayTracker.defaultDisplayId,
motionEventHandlerReceiver = {},
)
.apply { setTransitionState(transitionState) }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
index 680df15..dcf32a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerTest.kt
@@ -137,7 +137,7 @@
MediaTestUtils.emptyMediaData.copy(
app = PACKAGE,
packageName = PACKAGE,
- token = session.sessionToken
+ token = session.sessionToken,
)
resumeData = mediaData.copy(token = null, active = false, resumption = true)
@@ -237,7 +237,7 @@
// Assuming we're registered
testOnMediaDataLoaded_registersPlaybackListener()
- mediaCallbackCaptor.value.onPlaybackStateChanged(
+ onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
assertThat(mainExecutor.numPending()).isEqualTo(1)
@@ -249,7 +249,7 @@
// Assuming we have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
- mediaCallbackCaptor.value.onPlaybackStateChanged(
+ onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 0f).build()
)
assertThat(mainExecutor.numPending()).isEqualTo(0)
@@ -261,7 +261,7 @@
// Assuming we have a pending timeout
testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
- mediaCallbackCaptor.value.onPlaybackStateChanged(
+ onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
)
assertThat(mainExecutor.numPending()).isEqualTo(1)
@@ -435,7 +435,7 @@
// When the playback state changes, and has different actions
val playingState = PlaybackState.Builder().setActions(PlaybackState.ACTION_PLAY).build()
- mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+ onPlaybackStateChanged(playingState)
assertThat(uiExecutor.runAllReady()).isEqualTo(1)
// Then the callback is invoked
@@ -448,7 +448,7 @@
PlaybackState.CustomAction.Builder(
"ACTION_1",
"custom action 1",
- android.R.drawable.ic_media_ff
+ android.R.drawable.ic_media_ff,
)
.build()
val pausedState =
@@ -463,7 +463,7 @@
PlaybackState.CustomAction.Builder(
"ACTION_2",
"custom action 2",
- android.R.drawable.ic_media_rew
+ android.R.drawable.ic_media_rew,
)
.build()
val pausedStateTwoActions =
@@ -472,7 +472,7 @@
.addCustomAction(customOne)
.addCustomAction(customTwo)
.build()
- mediaCallbackCaptor.value.onPlaybackStateChanged(pausedStateTwoActions)
+ onPlaybackStateChanged(pausedStateTwoActions)
assertThat(uiExecutor.runAllReady()).isEqualTo(1)
// Then the callback is invoked
@@ -485,7 +485,7 @@
loadMediaDataWithPlaybackState(stateWithActions)
// When the playback state updates with the same actions
- mediaCallbackCaptor.value.onPlaybackStateChanged(stateWithActions)
+ onPlaybackStateChanged(stateWithActions)
// Then the callback is not invoked again
verify(stateCallback, never()).invoke(eq(KEY), any())
@@ -512,7 +512,7 @@
.setActions(PlaybackState.ACTION_PAUSE)
.addCustomAction(customTwo)
.build()
- mediaCallbackCaptor.value.onPlaybackStateChanged(stateTwo)
+ onPlaybackStateChanged(stateTwo)
// Then the callback is not invoked
verify(stateCallback, never()).invoke(eq(KEY), any())
@@ -544,7 +544,7 @@
// When the playback state changes to playing
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build()
- mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+ onPlaybackStateChanged(playingState)
uiExecutor.runAllReady()
// Then the callback is invoked
@@ -561,7 +561,7 @@
// When the playback state is updated, but still not playing
val playingState =
PlaybackState.Builder().setState(PlaybackState.STATE_STOPPED, 0L, 0f).build()
- mediaCallbackCaptor.value.onPlaybackStateChanged(playingState)
+ onPlaybackStateChanged(playingState)
// Then the callback is not invoked
verify(stateCallback, never()).invoke(eq(KEY), eq(playingState!!))
@@ -571,7 +571,7 @@
fun testTimeoutCallback_dozedPastTimeout_invokedOnWakeup() {
// When paused media is loaded
testOnMediaDataLoaded_registersPlaybackListener()
- mediaCallbackCaptor.value.onPlaybackStateChanged(
+ onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
@@ -597,7 +597,7 @@
val time = clock.currentTimeMillis()
clock.setElapsedRealtime(time)
testOnMediaDataLoaded_registersPlaybackListener()
- mediaCallbackCaptor.value.onPlaybackStateChanged(
+ onPlaybackStateChanged(
PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 0f).build()
)
verify(statusBarStateController).addCallback(capture(dozingCallbackCaptor))
@@ -706,4 +706,9 @@
bgExecutor.runAllReady()
uiExecutor.runAllReady()
}
+
+ private fun onPlaybackStateChanged(state: PlaybackState) {
+ mediaCallbackCaptor.value.onPlaybackStateChanged(state)
+ bgExecutor.runAllReady()
+ }
}
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 eea02ee..2f8f45c 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
@@ -29,6 +29,7 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -887,6 +888,34 @@
}
@Test
+ public void getActiveAutoSwitchNonDdsSubId_registerCallbackForExistedSubId_notRegister() {
+ mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
+
+ // Adds non DDS subId
+ SubscriptionInfo info = mock(SubscriptionInfo.class);
+ doReturn(SUB_ID2).when(info).getSubscriptionId();
+ doReturn(false).when(info).isOpportunistic();
+ when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+ mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+
+ // 1st time is onStart(), 2nd time is getActiveAutoSwitchNonDdsSubId()
+ verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any());
+ assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2);
+
+ // Adds non DDS subId again
+ doReturn(SUB_ID2).when(info).getSubscriptionId();
+ doReturn(false).when(info).isOpportunistic();
+ when(mSubscriptionManager.getActiveSubscriptionInfo(anyInt())).thenReturn(info);
+
+ mInternetDialogController.getActiveAutoSwitchNonDdsSubId();
+
+ // Does not add due to cached subInfo in mSubIdTelephonyCallbackMap.
+ verify(mTelephonyManager, times(2)).registerTelephonyCallback(any(), any());
+ assertThat(mInternetDialogController.mSubIdTelephonyCallbackMap.size() == 2);
+ }
+
+ @Test
public void getMobileNetworkSummary() {
mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, true);
Resources res1 = mock(Resources.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
index a1750cd..b1ec740 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -21,6 +21,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.OverlayKey
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
@@ -28,6 +29,7 @@
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.data.repository.setSceneTransition
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
@@ -38,9 +40,9 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -55,14 +57,9 @@
private val configurationRepository = kosmos.fakeConfigurationRepository
private val keyguardRepository = kosmos.fakeKeyguardRepository
private val sceneInteractor = kosmos.sceneInteractor
- private val shadeTestUtil = kosmos.shadeTestUtil
+ private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
- private lateinit var underTest: ShadeInteractorSceneContainerImpl
-
- @Before
- fun setUp() {
- underTest = kosmos.shadeInteractorSceneContainerImpl
- }
+ private val underTest by lazy { kosmos.shadeInteractorSceneContainerImpl }
@Test
fun qsExpansionWhenInSplitShadeAndQsExpanded() =
@@ -600,14 +597,14 @@
@Test
@EnableFlags(DualShade.FLAG_NAME)
- fun expandNotificationShade_dualShadeEnabled_opensOverlay() =
+ fun expandNotificationsShade_dualShade_opensOverlay() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
assertThat(currentOverlays).isEmpty()
- underTest.expandNotificationShade("reason")
+ underTest.expandNotificationsShade("reason")
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
assertThat(currentOverlays).containsExactly(Overlays.NotificationsShade)
@@ -615,14 +612,15 @@
@Test
@DisableFlags(DualShade.FLAG_NAME)
- fun expandNotificationShade_dualShadeDisabled_switchesToShadeScene() =
+ fun expandNotificationsShade_singleShade_switchesToShadeScene() =
testScope.runTest {
+ shadeTestUtil.setSplitShade(false)
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
assertThat(currentOverlays).isEmpty()
- underTest.expandNotificationShade("reason")
+ underTest.expandNotificationsShade("reason")
assertThat(currentScene).isEqualTo(Scenes.Shade)
assertThat(currentOverlays).isEmpty()
@@ -630,7 +628,7 @@
@Test
@EnableFlags(DualShade.FLAG_NAME)
- fun expandNotificationShade_dualShadeEnabledAndQuickSettingsOpen_replacesOverlay() =
+ fun expandNotificationsShade_dualShadeQuickSettingsOpen_replacesOverlay() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
@@ -638,14 +636,14 @@
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
assertThat(currentOverlays).containsExactly(Overlays.QuickSettingsShade)
- underTest.expandNotificationShade("reason")
+ underTest.expandNotificationsShade("reason")
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
assertThat(currentOverlays).containsExactly(Overlays.NotificationsShade)
}
@Test
@EnableFlags(DualShade.FLAG_NAME)
- fun expandQuickSettingsShade_dualShadeEnabled_opensOverlay() =
+ fun expandQuickSettingsShade_dualShade_opensOverlay() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
@@ -660,8 +658,9 @@
@Test
@DisableFlags(DualShade.FLAG_NAME)
- fun expandQuickSettingsShade_dualShadeDisabled_switchesToQuickSettingsScene() =
+ fun expandQuickSettingsShade_singleShade_switchesToQuickSettingsScene() =
testScope.runTest {
+ shadeTestUtil.setSplitShade(false)
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
@@ -674,12 +673,28 @@
}
@Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun expandQuickSettingsShade_splitShade_switchesToShadeScene() =
+ testScope.runTest {
+ shadeTestUtil.setSplitShade(true)
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(currentOverlays).isEmpty()
+
+ underTest.expandQuickSettingsShade("reason")
+
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+ assertThat(currentOverlays).isEmpty()
+ }
+
+ @Test
@EnableFlags(DualShade.FLAG_NAME)
- fun expandQuickSettingsShade_dualShadeEnabledAndNotificationsOpen_replacesOverlay() =
+ fun expandQuickSettingsShade_dualShadeNotificationsOpen_replacesOverlay() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
- underTest.expandNotificationShade("reason")
+ underTest.expandNotificationsShade("reason")
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
assertThat(currentOverlays).containsExactly(Overlays.NotificationsShade)
@@ -687,4 +702,141 @@
assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
assertThat(currentOverlays).containsExactly(Overlays.QuickSettingsShade)
}
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun collapseNotificationsShade_dualShade_hidesOverlay() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ openShade(Overlays.NotificationsShade)
+
+ underTest.collapseNotificationsShade("reason")
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(currentOverlays).isEmpty()
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun collapseNotificationsShade_singleShade_switchesToLockscreen() =
+ testScope.runTest {
+ shadeTestUtil.setSplitShade(false)
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ sceneInteractor.changeScene(Scenes.Shade, "reason")
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+ assertThat(currentOverlays).isEmpty()
+
+ underTest.collapseNotificationsShade("reason")
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(currentOverlays).isEmpty()
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun collapseQuickSettingsShade_dualShade_hidesOverlay() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ openShade(Overlays.QuickSettingsShade)
+
+ underTest.collapseQuickSettingsShade("reason")
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(currentOverlays).isEmpty()
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun collapseQuickSettingsShadeNotBypassingShade_singleShade_switchesToShade() =
+ testScope.runTest {
+ shadeTestUtil.setSplitShade(false)
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ sceneInteractor.changeScene(Scenes.QuickSettings, "reason")
+ assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+ assertThat(currentOverlays).isEmpty()
+
+ underTest.collapseQuickSettingsShade(
+ loggingReason = "reason",
+ bypassNotificationsShade = false,
+ )
+
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+ assertThat(currentOverlays).isEmpty()
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun collapseQuickSettingsShadeNotBypassingShade_splitShade_switchesToLockscreen() =
+ testScope.runTest {
+ shadeTestUtil.setSplitShade(true)
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ sceneInteractor.changeScene(Scenes.QuickSettings, "reason")
+ assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+ assertThat(currentOverlays).isEmpty()
+
+ underTest.collapseQuickSettingsShade(
+ loggingReason = "reason",
+ bypassNotificationsShade = false,
+ )
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(currentOverlays).isEmpty()
+ }
+
+ @Test
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun collapseQuickSettingsShadeBypassingShade_singleShade_switchesToLockscreen() =
+ testScope.runTest {
+ shadeTestUtil.setSplitShade(false)
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ sceneInteractor.changeScene(Scenes.QuickSettings, "reason")
+ assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+ assertThat(currentOverlays).isEmpty()
+
+ underTest.collapseQuickSettingsShade(
+ loggingReason = "reason",
+ bypassNotificationsShade = true,
+ )
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(currentOverlays).isEmpty()
+ }
+
+ @Test
+ @EnableFlags(DualShade.FLAG_NAME)
+ fun collapseEitherShade_dualShade_hidesBothOverlays() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ openShade(Overlays.QuickSettingsShade)
+ openShade(Overlays.NotificationsShade)
+ assertThat(currentOverlays)
+ .containsExactly(Overlays.QuickSettingsShade, Overlays.NotificationsShade)
+
+ underTest.collapseEitherShade("reason")
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ assertThat(currentOverlays).isEmpty()
+ }
+
+ private fun TestScope.openShade(overlay: OverlayKey) {
+ val isAnyExpanded by collectLastValue(underTest.isAnyExpanded)
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ val initialScene = checkNotNull(currentScene)
+ sceneInteractor.showOverlay(overlay, "reason")
+ kosmos.setSceneTransition(
+ ObservableTransitionState.Idle(initialScene, checkNotNull(currentOverlays))
+ )
+ runCurrent()
+ assertThat(currentScene).isEqualTo(initialScene)
+ assertThat(currentOverlays).contains(overlay)
+ assertThat(isAnyExpanded).isTrue()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
index 0407fc1..ac73882 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationSectionsFeatureManagerTest.kt
@@ -24,7 +24,7 @@
import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_USE_PEOPLE_FILTERING
import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.notification.shared.NotificationMinimalismPrototype
+import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
import com.android.systemui.util.DeviceConfigProxyFake
import com.android.systemui.util.Utils
@@ -42,7 +42,7 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
// this class has no testable logic with either of these flags enabled
-@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalismPrototype.FLAG_NAME)
+@DisableFlags(PriorityPeopleSection.FLAG_NAME, NotificationMinimalism.FLAG_NAME)
class NotificationSectionsFeatureManagerTest : SysuiTestCase() {
lateinit var manager: NotificationSectionsFeatureManager
private val proxyFake = DeviceConfigProxyFake()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
index 07c29a0..0c65c9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
@@ -20,7 +20,6 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
@@ -30,15 +29,12 @@
import android.app.Notification.MediaStyle;
import android.media.session.MediaSession;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
import android.service.notification.NotificationListenerService;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
import com.android.systemui.statusbar.notification.InflationException;
@@ -158,8 +154,7 @@
}
@Test
- @DisableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
- public void inflateMediaNotificationIconsMediaEnabled_old() throws InflationException {
+ public void inflateMediaNotificationIconsMediaEnabled() throws InflationException {
finishSetupWithMediaFeatureFlagEnabled(true);
mListener.onEntryInit(mMediaEntry);
@@ -187,37 +182,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
- public void inflateMediaNotificationIconsMediaEnabled_new() throws InflationException {
- finishSetupWithMediaFeatureFlagEnabled(true);
-
- mListener.onEntryInit(mMediaEntry);
- mListener.onEntryAdded(mMediaEntry);
- verify(mIconManager).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
- clearInvocations(mIconManager);
-
- mFilter.shouldFilterOut(mMediaEntry, 0);
- verify(mIconManager, never()).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
-
- mListener.onEntryUpdated(mMediaEntry);
- verify(mIconManager, never()).createIcons(eq(mMediaEntry));
- verify(mIconManager).updateIcons(eq(mMediaEntry), /* usingCache = */ eq(false));
-
- mListener.onEntryRemoved(mMediaEntry, NotificationListenerService.REASON_CANCEL);
- mListener.onEntryCleanUp(mMediaEntry);
- clearInvocations(mIconManager);
-
- mListener.onEntryInit(mMediaEntry);
- mListener.onEntryAdded(mMediaEntry);
- verify(mIconManager).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
- }
-
- @Test
- @DisableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
- public void inflationException_old() throws InflationException {
+ public void inflationException() throws InflationException {
finishSetupWithMediaFeatureFlagEnabled(true);
mListener.onEntryInit(mMediaEntry);
@@ -244,31 +209,6 @@
verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
}
- @Test
- @EnableFlags(Flags.FLAG_NOTIFICATIONS_BACKGROUND_ICONS)
- public void inflationException_new() throws InflationException {
- finishSetupWithMediaFeatureFlagEnabled(true);
-
- doThrow(InflationException.class).when(mIconManager).createIcons(eq(mMediaEntry));
-
- mListener.onEntryInit(mMediaEntry);
- mListener.onEntryAdded(mMediaEntry);
- verify(mIconManager).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
- clearInvocations(mIconManager);
-
- mListener.onEntryUpdated(mMediaEntry);
- verify(mIconManager).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
- clearInvocations(mIconManager);
-
- doNothing().when(mIconManager).createIcons(eq(mMediaEntry));
-
- mListener.onEntryUpdated(mMediaEntry);
- verify(mIconManager).createIcons(eq(mMediaEntry));
- verify(mIconManager, never()).updateIcons(eq(mMediaEntry), anyBoolean());
- }
-
private void finishSetupWithMediaFeatureFlagEnabled(boolean mediaFeatureFlagEnabled) {
when(mMediaFeatureFlag.getEnabled()).thenReturn(mediaFeatureFlagEnabled);
mCoordinator = new MediaCoordinator(mMediaFeatureFlag, mStatusBarService, mIconManager);
diff --git a/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt
new file mode 100644
index 0000000..cd681a1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/view/WindowManagerServiceKosmos.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view
+
+import android.graphics.Region
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+
+val Kosmos.mockWindowManagerService: IWindowManager by
+ Kosmos.Fixture {
+ mock(IWindowManager::class.java).apply {
+ whenever(registerSystemGestureExclusionListener(any(), anyInt())).then { answer ->
+ val listener = answer.arguments[0] as ISystemGestureExclusionListener
+ val displayId = answer.arguments[1] as Int
+ exclusionListeners.getOrPut(displayId) { mutableListOf() }.add(listener)
+ listener.onSystemGestureExclusionChanged(
+ displayId,
+ restrictedRegionByDisplayId[displayId],
+ null,
+ )
+ }
+
+ whenever(unregisterSystemGestureExclusionListener(any(), anyInt())).then { answer ->
+ val listener = answer.arguments[0] as ISystemGestureExclusionListener
+ val displayId = answer.arguments[1] as Int
+ exclusionListeners[displayId]?.remove(listener)
+ }
+ }
+ }
+
+var Kosmos.windowManagerService: IWindowManager by Kosmos.Fixture { mockWindowManagerService }
+
+private var restrictedRegionByDisplayId = mutableMapOf<Int, Region?>()
+private var exclusionListeners = mutableMapOf<Int, MutableList<ISystemGestureExclusionListener>>()
+
+fun setSystemGestureExclusionRegion(displayId: Int, restrictedRegion: Region?) {
+ restrictedRegionByDisplayId[displayId] = restrictedRegion
+ exclusionListeners[displayId]?.forEach { listener ->
+ listener.onSystemGestureExclusionChanged(displayId, restrictedRegion, null)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
index e2b283b..38bc758 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissActionInteractorKosmos.kt
@@ -22,7 +22,6 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.power.domain.interactor.powerInteractor
-import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -34,7 +33,6 @@
transitionInteractor = keyguardTransitionInteractor,
dismissInteractor = keyguardDismissInteractor,
applicationScope = testScope.backgroundScope,
- sceneInteractor = { sceneInteractor },
deviceUnlockedInteractor = { deviceUnlockedInteractor },
powerInteractor = powerInteractor,
alternateBouncerInteractor = alternateBouncerInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
index ff8b478..a80a409 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
@@ -17,13 +17,13 @@
package com.android.systemui.qs.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory
val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayContentViewModel by
Kosmos.Fixture {
QuickSettingsShadeOverlayContentViewModel(
- sceneInteractor = sceneInteractor,
+ shadeInteractor = shadeInteractor,
shadeHeaderViewModelFactory = shadeHeaderViewModelFactory,
quickSettingsContainerViewModel = quickSettingsContainerViewModel,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index 8744638..737aaf2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -6,13 +6,16 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.interactor.systemGestureExclusionInteractor
import com.android.systemui.scene.shared.logger.sceneLogger
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.FakeOverlay
+import com.android.systemui.scene.ui.viewmodel.SceneContainerGestureFilter
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.settings.displayTracker
import com.android.systemui.shade.domain.interactor.shadeInteractor
import kotlinx.coroutines.flow.MutableStateFlow
@@ -30,10 +33,7 @@
val Kosmos.initialSceneKey by Fixture { Scenes.Lockscreen }
var Kosmos.overlayKeys by Fixture {
- listOf(
- Overlays.NotificationsShade,
- Overlays.QuickSettingsShade,
- )
+ listOf(Overlays.NotificationsShade, Overlays.QuickSettingsShade)
}
val Kosmos.fakeOverlaysByKeys by Fixture { overlayKeys.associateWith { FakeOverlay(it) } }
@@ -74,8 +74,21 @@
powerInteractor = powerInteractor,
shadeInteractor = shadeInteractor,
splitEdgeDetector = splitEdgeDetector,
+ gestureFilterFactory = sceneContainerGestureFilterFactory,
+ displayId = displayTracker.defaultDisplayId,
motionEventHandlerReceiver = {},
- logger = sceneLogger
+ logger = sceneLogger,
)
.apply { setTransitionState(transitionState) }
}
+
+val Kosmos.sceneContainerGestureFilterFactory by Fixture {
+ object : SceneContainerGestureFilter.Factory {
+ override fun create(displayId: Int): SceneContainerGestureFilter {
+ return SceneContainerGestureFilter(
+ interactor = systemGestureExclusionInteractor,
+ displayId = displayId,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt
new file mode 100644
index 0000000..15ed1b3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SystemGestureExclusionRepositoryKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.data.repository
+
+import android.view.windowManagerService
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.systemGestureExclusionRepository by Fixture {
+ SystemGestureExclusionRepository(windowManager = windowManagerService)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt
new file mode 100644
index 0000000..3e46c3f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SystemGestureExclusionInteractorKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.scene.data.repository.systemGestureExclusionRepository
+
+val Kosmos.systemGestureExclusionInteractor by Fixture {
+ SystemGestureExclusionInteractor(repository = systemGestureExclusionRepository)
+}
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 6d488d2..60141c6 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
@@ -25,6 +25,7 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.FakeShadeRepository
import com.android.systemui.shade.data.repository.ShadeRepository
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
@@ -133,7 +134,7 @@
class ShadeTestUtilLegacyImpl(
val testScope: TestScope,
val shadeRepository: FakeShadeRepository,
- val context: SysuiTestableContext
+ val context: SysuiTestableContext,
) : ShadeTestUtilDelegate {
override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
shadeRepository.setLegacyShadeExpansion(shadeExpansion)
@@ -191,6 +192,7 @@
}
/** Sets up shade state for tests when the scene container flag is enabled. */
+@OptIn(ExperimentalCoroutinesApi::class)
class ShadeTestUtilSceneImpl(
val testScope: TestScope,
val sceneInteractor: SceneInteractor,
@@ -269,7 +271,7 @@
from: SceneKey,
to: SceneKey,
progress: Float,
- isInitiatedByUserInput: Boolean = true
+ isInitiatedByUserInput: Boolean = true,
) {
sceneInteractor.changeScene(from, "test")
val transitionState =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
index 9cdd519..7a15fdf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeOverlayContentViewModelKosmos.kt
@@ -19,7 +19,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
-import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModelFactory
val Kosmos.notificationsShadeOverlayContentViewModel:
@@ -27,6 +27,6 @@
NotificationsShadeOverlayContentViewModel(
shadeHeaderViewModelFactory = shadeHeaderViewModelFactory,
notificationsPlaceholderViewModelFactory = notificationsPlaceholderViewModelFactory,
- sceneInteractor = sceneInteractor,
+ shadeInteractor = shadeInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
index 7eb9f34..f5b856d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
@@ -20,7 +20,6 @@
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.plugins.activityStarter
-import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.privacyChipInteractor
import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -32,7 +31,6 @@
ShadeHeaderViewModel(
context = applicationContext,
activityStarter = activityStarter,
- sceneInteractor = sceneInteractor,
shadeInteractor = shadeInteractor,
mobileIconsInteractor = mobileIconsInteractor,
mobileIconsViewModel = mobileIconsViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
index b19e221..3d2bd6c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/SeenNotificationsInteractorKosmos.kt
@@ -45,3 +45,17 @@
UserHandle.USER_CURRENT,
)
}
+
+var Kosmos.lockScreenNotificationMinimalismSetting: Boolean
+ get() =
+ fakeSettings.getIntForUser(
+ Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+ UserHandle.USER_CURRENT,
+ ) == 1
+ set(value) {
+ fakeSettings.putIntForUser(
+ Settings.Secure.LOCK_SCREEN_NOTIFICATION_MINIMALISM,
+ if (value) 1 else 0,
+ UserHandle.USER_CURRENT,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.kt
new file mode 100644
index 0000000..8fdb948
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/emptyshade/ui/viewmodel/EmptyShadeViewModelKosmos.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.statusbar.notification.emptyshade.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shared.notifications.domain.interactor.notificationSettingsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
+
+val Kosmos.emptyShadeViewModel by
+ Kosmos.Fixture {
+ EmptyShadeViewModel(
+ applicationContext,
+ zenModeInteractor,
+ seenNotificationsInteractor,
+ notificationSettingsInteractor,
+ dumpManager,
+ )
+ }
+
+val Kosmos.emptyShadeViewModelFactory: EmptyShadeViewModel.Factory by
+ Kosmos.Fixture {
+ object : EmptyShadeViewModel.Factory {
+ override fun create() = emptyShadeViewModel
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
index de8b350..c3bc744 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt
@@ -23,13 +23,12 @@
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
-import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor
+import com.android.systemui.statusbar.notification.emptyshade.ui.viewmodel.emptyShadeViewModelFactory
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.notificationShelfViewModel
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor
import com.android.systemui.statusbar.policy.domain.interactor.userSetupInteractor
-import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
import java.util.Optional
val Kosmos.notificationListViewModel by Fixture {
@@ -37,15 +36,14 @@
notificationShelfViewModel,
hideListViewModel,
Optional.of(footerViewModel),
+ emptyShadeViewModelFactory,
Optional.of(notificationListLoggerViewModel),
activeNotificationsInteractor,
notificationStackInteractor,
headsUpNotificationInteractor,
remoteInputInteractor,
- seenNotificationsInteractor,
shadeInteractor,
userSetupInteractor,
- zenModeInteractor,
testDispatcher,
dumpManager,
)
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 3224b27..73b7b35 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -165,16 +165,27 @@
protected final AccessibilitySecurityPolicy mSecurityPolicy;
protected final AccessibilityTrace mTrace;
- // The attribution tag set by the service that is bound to this instance
+ /** The attribution tag set by the client that is bound to this instance */
protected String mAttributionTag;
protected int mDisplayTypes = DISPLAY_TYPE_DEFAULT;
- // The service that's bound to this instance. Whenever this value is non-null, this
- // object is registered as a death recipient
- IBinder mService;
+ /**
+ * Binder of the {@link #mClient}.
+ *
+ * <p>Whenever this value is non-null, it should be registered as a {@link
+ * IBinder.DeathRecipient}
+ */
+ @Nullable IBinder mClientBinder;
- IAccessibilityServiceClient mServiceInterface;
+ /**
+ * The accessibility client this class represents.
+ *
+ * <p>The client is in the application process, i.e., it's a client of system_server. Depending
+ * on the use case, the client can be an {@link AccessibilityService}, a {@code UiAutomation},
+ * etc.
+ */
+ @Nullable IAccessibilityServiceClient mClient;
int mEventTypes;
@@ -218,10 +229,10 @@
int mGenericMotionEventSources;
int mObservedMotionEventSources;
- // the events pending events to be dispatched to this service
+ /** Pending events to be dispatched to the client */
final SparseArray<AccessibilityEvent> mPendingEvents = new SparseArray<>();
- /** Whether this service relies on its {@link AccessibilityCache} being up to date */
+ /** Whether the client relies on its {@link AccessibilityCache} being up to date */
boolean mUsesAccessibilityCache = false;
// Handler only for dispatching accessibility events since we use event
@@ -230,7 +241,7 @@
final SparseArray<IBinder> mOverlayWindowTokens = new SparseArray();
- // All the embedded accessibility overlays that have been added by this service.
+ /** All the embedded accessibility overlays that have been added by the client. */
private List<SurfaceControl> mOverlays = new ArrayList<>();
/** The timestamp of requesting to take screenshot in milliseconds */
@@ -274,7 +285,8 @@
/**
* Called back to notify system that the client has changed
- * @param serviceInfoChanged True if the service's AccessibilityServiceInfo changed.
+ *
+ * @param serviceInfoChanged True if the client's AccessibilityServiceInfo changed.
*/
void onClientChangeLocked(boolean serviceInfoChanged);
@@ -360,21 +372,22 @@
mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mIPlatformCompat = IPlatformCompat.Stub.asInterface(
ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE));
- mEventDispatchHandler = new Handler(mainHandler.getLooper()) {
- @Override
- public void handleMessage(Message message) {
- final int eventType = message.what;
- AccessibilityEvent event = (AccessibilityEvent) message.obj;
- boolean serviceWantsEvent = message.arg1 != 0;
- notifyAccessibilityEventInternal(eventType, event, serviceWantsEvent);
- }
- };
+ mEventDispatchHandler =
+ new Handler(mainHandler.getLooper()) {
+ @Override
+ public void handleMessage(Message message) {
+ final int eventType = message.what;
+ AccessibilityEvent event = (AccessibilityEvent) message.obj;
+ boolean clientWantsEvent = message.arg1 != 0;
+ notifyAccessibilityEventInternal(eventType, event, clientWantsEvent);
+ }
+ };
setDynamicallyConfigurableProperties(accessibilityServiceInfo);
}
@Override
public boolean onKeyEvent(KeyEvent keyEvent, int sequenceNumber) {
- if (!mRequestFilterKeyEvents || (mServiceInterface == null)) {
+ if (!mRequestFilterKeyEvents || (mClient == null)) {
return false;
}
if((mAccessibilityServiceInfo.getCapabilities()
@@ -388,7 +401,7 @@
if (svcClientTracingEnabled()) {
logTraceSvcClient("onKeyEvent", keyEvent + ", " + sequenceNumber);
}
- mServiceInterface.onKeyEvent(keyEvent, sequenceNumber);
+ mClient.onKeyEvent(keyEvent, sequenceNumber);
} catch (RemoteException e) {
return false;
}
@@ -470,7 +483,7 @@
}
public boolean canReceiveEventsLocked() {
- return (mEventTypes != 0 && mService != null);
+ return (mEventTypes != 0 && mClientBinder != null);
}
@RequiresNoPermission
@@ -520,7 +533,7 @@
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- // If the XML manifest had data to configure the service its info
+ // If the XML manifest had data to configure the AccessibilityService, its info
// should be already set. In such a case update only the dynamically
// configurable properties.
boolean oldRequestIme = mRequestImeApis;
@@ -1733,40 +1746,40 @@
try {
// Clear the proxy in the other process so this
// IAccessibilityServiceConnection can be garbage collected.
- if (mServiceInterface != null) {
+ if (mClient != null) {
if (svcClientTracingEnabled()) {
logTraceSvcClient("init", "null, " + mId + ", null");
}
- mServiceInterface.init(null, mId, null);
+ mClient.init(null, mId, null);
}
} catch (RemoteException re) {
/* ignore */
}
- if (mService != null) {
+ if (mClientBinder != null) {
try {
- mService.unlinkToDeath(this, 0);
+ mClientBinder.unlinkToDeath(this, 0);
} catch (NoSuchElementException e) {
Slog.e(LOG_TAG, "Failed unregistering death link");
}
- mService = null;
+ mClientBinder = null;
}
- mServiceInterface = null;
+ mClient = null;
mReceivedAccessibilityButtonCallbackSinceBind = false;
}
public boolean isConnectedLocked() {
- return (mService != null);
+ return (mClientBinder != null);
}
public void notifyAccessibilityEvent(AccessibilityEvent event) {
synchronized (mLock) {
final int eventType = event.getEventType();
- final boolean serviceWantsEvent = wantsEventLocked(event);
+ final boolean clientWantsEvent = clientWantsEventLocked(event);
final boolean requiredForCacheConsistency = mUsesAccessibilityCache
&& ((AccessibilityCache.CACHE_CRITICAL_EVENTS_MASK & eventType) != 0);
- if (!serviceWantsEvent && !requiredForCacheConsistency) {
+ if (!clientWantsEvent && !requiredForCacheConsistency) {
return;
}
@@ -1774,7 +1787,7 @@
return;
}
// Make a copy since during dispatch it is possible the event to
- // be modified to remove its source if the receiving service does
+ // be modified to remove its source if the receiving client does
// not have permission to access the window content.
AccessibilityEvent newEvent = AccessibilityEvent.obtain(event);
Message message;
@@ -1792,22 +1805,20 @@
// Send all messages, bypassing mPendingEvents
message = mEventDispatchHandler.obtainMessage(eventType, newEvent);
}
- message.arg1 = serviceWantsEvent ? 1 : 0;
+ message.arg1 = clientWantsEvent ? 1 : 0;
mEventDispatchHandler.sendMessageDelayed(message, mNotificationTimeout);
}
}
/**
- * Determines if given event can be dispatched to a service based on the package of the
- * event source. Specifically, a service is notified if it is interested in events from the
- * package.
+ * Determines if given event can be dispatched to a client based on the package of the event
+ * source. Specifically, a client is notified if it is interested in events from the package.
*
* @param event The event.
- * @return True if the listener should be notified, false otherwise.
+ * @return True if the client should be notified, false otherwise.
*/
- private boolean wantsEventLocked(AccessibilityEvent event) {
-
+ private boolean clientWantsEventLocked(AccessibilityEvent event) {
if (!canReceiveEventsLocked()) {
return false;
}
@@ -1838,22 +1849,20 @@
}
/**
- * Notifies an accessibility service client for a scheduled event given the event type.
+ * Notifies a client for a scheduled event given the event type.
*
* @param eventType The type of the event to dispatch.
*/
private void notifyAccessibilityEventInternal(
- int eventType,
- AccessibilityEvent event,
- boolean serviceWantsEvent) {
- IAccessibilityServiceClient listener;
+ int eventType, AccessibilityEvent event, boolean clientWantsEvent) {
+ IAccessibilityServiceClient client;
synchronized (mLock) {
- listener = mServiceInterface;
+ client = mClient;
- // If the service died/was disabled while the message for dispatching
- // the accessibility event was propagating the listener may be null.
- if (listener == null) {
+ // If the client (in the application process) died/was disabled while the message for
+ // dispatching the accessibility event was propagating, "client" may be null.
+ if (client == null) {
return;
}
@@ -1868,7 +1877,7 @@
// 1) A binder thread calls notifyAccessibilityServiceDelayedLocked
// which posts a message for dispatching an event and stores the event
// in mPendingEvents.
- // 2) The message is pulled from the queue by the handler on the service
+ // 2) The message is pulled from the queue by the handler on the client
// thread and this method is just about to acquire the lock.
// 3) Another binder thread acquires the lock in notifyAccessibilityEvent
// 4) notifyAccessibilityEvent recycles the event that this method was about
@@ -1876,7 +1885,7 @@
// 5) This method grabs the new event, processes it, and removes it from
// mPendingEvents
// 6) The second message dispatched in (4) arrives, but the event has been
- // remvoved in (5).
+ // removed in (5).
event = mPendingEvents.get(eventType);
if (event == null) {
return;
@@ -1893,14 +1902,14 @@
try {
if (svcClientTracingEnabled()) {
- logTraceSvcClient("onAccessibilityEvent", event + ";" + serviceWantsEvent);
+ logTraceSvcClient("onAccessibilityEvent", event + ";" + clientWantsEvent);
}
- listener.onAccessibilityEvent(event, serviceWantsEvent);
+ client.onAccessibilityEvent(event, clientWantsEvent);
if (DEBUG) {
- Slog.i(LOG_TAG, "Event " + event + " sent to " + listener);
+ Slog.i(LOG_TAG, "Event " + event + " sent to " + client);
}
} catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error during sending " + event + " to " + listener, re);
+ Slog.e(LOG_TAG, "Error during sending " + event + " to " + client, re);
} finally {
event.recycle();
}
@@ -1978,122 +1987,126 @@
return (mGenericMotionEventSources & eventSourceWithoutClass) != 0;
}
-
/**
- * Called by the invocation handler to notify the service that the
- * state of magnification has changed.
+ * Called by the invocation handler to notify the client that the state of magnification has
+ * changed.
*/
- private void notifyMagnificationChangedInternal(int displayId, @NonNull Region region,
- @NonNull MagnificationConfig config) {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ private void notifyMagnificationChangedInternal(
+ int displayId, @NonNull Region region, @NonNull MagnificationConfig config) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("onMagnificationChanged", displayId + ", " + region + ", "
+ config.toString());
}
- listener.onMagnificationChanged(displayId, region, config);
+ client.onMagnificationChanged(displayId, region, config);
} catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error sending magnification changes to " + mService, re);
+ Slog.e(LOG_TAG, "Error sending magnification changes to " + mClientBinder, re);
}
}
}
/**
- * Called by the invocation handler to notify the service that the state of the soft
- * keyboard show mode has changed.
+ * Called by the invocation handler to notify the client that the state of the soft keyboard
+ * show mode has changed.
*/
private void notifySoftKeyboardShowModeChangedInternal(int showState) {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("onSoftKeyboardShowModeChanged", String.valueOf(showState));
}
- listener.onSoftKeyboardShowModeChanged(showState);
+ client.onSoftKeyboardShowModeChanged(showState);
} catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error sending soft keyboard show mode changes to " + mService,
+ Slog.e(
+ LOG_TAG,
+ "Error sending soft keyboard show mode changes to " + mClientBinder,
re);
}
}
}
private void notifyAccessibilityButtonClickedInternal(int displayId) {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("onAccessibilityButtonClicked", String.valueOf(displayId));
}
- listener.onAccessibilityButtonClicked(displayId);
+ client.onAccessibilityButtonClicked(displayId);
} catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error sending accessibility button click to " + mService, re);
+ Slog.e(LOG_TAG, "Error sending accessibility button click to " + mClientBinder, re);
}
}
}
private void notifyAccessibilityButtonAvailabilityChangedInternal(boolean available) {
- // Only notify the service if it's not been notified or the state has changed
+ // Only notify the client if it's not been notified or the state has changed
if (mReceivedAccessibilityButtonCallbackSinceBind
&& (mLastAccessibilityButtonCallbackState == available)) {
return;
}
mReceivedAccessibilityButtonCallbackSinceBind = true;
mLastAccessibilityButtonCallbackState = available;
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("onAccessibilityButtonAvailabilityChanged",
String.valueOf(available));
}
- listener.onAccessibilityButtonAvailabilityChanged(available);
+ client.onAccessibilityButtonAvailabilityChanged(available);
} catch (RemoteException re) {
- Slog.e(LOG_TAG,
- "Error sending accessibility button availability change to " + mService,
+ Slog.e(
+ LOG_TAG,
+ "Error sending accessibility button availability change to "
+ + mClientBinder,
re);
}
}
}
private void notifyGestureInternal(AccessibilityGestureEvent gestureInfo) {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("onGesture", gestureInfo.toString());
}
- listener.onGesture(gestureInfo);
+ client.onGesture(gestureInfo);
} catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error during sending gesture " + gestureInfo
- + " to " + mService, re);
- }
- }
- }
-
- private void notifySystemActionsChangedInternal() {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
- try {
- if (svcClientTracingEnabled()) {
- logTraceSvcClient("onSystemActionsChanged", "");
- }
- listener.onSystemActionsChanged();
- } catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error sending system actions change to " + mService,
+ Slog.e(
+ LOG_TAG,
+ "Error during sending gesture " + gestureInfo + " to " + mClientBinder,
re);
}
}
}
+ private void notifySystemActionsChangedInternal() {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
+ try {
+ if (svcClientTracingEnabled()) {
+ logTraceSvcClient("onSystemActionsChanged", "");
+ }
+ client.onSystemActionsChanged();
+ } catch (RemoteException re) {
+ Slog.e(LOG_TAG, "Error sending system actions change to " + mClientBinder, re);
+ }
+ }
+ }
+
private void notifyClearAccessibilityCacheInternal() {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("clearAccessibilityCache", "");
}
- listener.clearAccessibilityCache();
+ client.clearAccessibilityCache();
} catch (RemoteException re) {
Slog.e(LOG_TAG, "Error during requesting accessibility info cache"
+ " to be cleared.", re);
@@ -2106,70 +2119,66 @@
private void setImeSessionEnabledInternal(IAccessibilityInputMethodSession session,
boolean enabled) {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null && session != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null && session != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("createImeSession", "");
}
- listener.setImeSessionEnabled(session, enabled);
+ client.setImeSessionEnabled(session, enabled);
} catch (RemoteException re) {
- Slog.e(LOG_TAG,
- "Error requesting IME session from " + mService, re);
+ Slog.e(LOG_TAG, "Error requesting IME session from " + mClientBinder, re);
}
}
}
private void bindInputInternal() {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("bindInput", "");
}
- listener.bindInput();
+ client.bindInput();
} catch (RemoteException re) {
- Slog.e(LOG_TAG,
- "Error binding input to " + mService, re);
+ Slog.e(LOG_TAG, "Error binding input to " + mClientBinder, re);
}
}
}
private void unbindInputInternal() {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("unbindInput", "");
}
- listener.unbindInput();
+ client.unbindInput();
} catch (RemoteException re) {
- Slog.e(LOG_TAG,
- "Error unbinding input to " + mService, re);
+ Slog.e(LOG_TAG, "Error unbinding input to " + mClientBinder, re);
}
}
}
private void startInputInternal(IRemoteAccessibilityInputConnection connection,
EditorInfo editorInfo, boolean restarting) {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("startInput", "editorInfo=" + editorInfo
+ " restarting=" + restarting);
}
- listener.startInput(connection, editorInfo, restarting);
+ client.startInput(connection, editorInfo, restarting);
} catch (RemoteException re) {
- Slog.e(LOG_TAG,
- "Error starting input to " + mService, re);
+ Slog.e(LOG_TAG, "Error starting input to " + mClientBinder, re);
}
}
}
- protected IAccessibilityServiceClient getServiceInterfaceSafely() {
+ protected IAccessibilityServiceClient getClientSafely() {
synchronized (mLock) {
- return mServiceInterface;
+ return mClient;
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 7580b69..d595d02 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -1435,8 +1435,8 @@
interfacesToInterrupt = new ArrayList<>(services.size());
for (int i = 0; i < services.size(); i++) {
AccessibilityServiceConnection service = services.get(i);
- IBinder a11yServiceBinder = service.mService;
- IAccessibilityServiceClient a11yServiceInterface = service.mServiceInterface;
+ IBinder a11yServiceBinder = service.mClientBinder;
+ IAccessibilityServiceClient a11yServiceInterface = service.mClient;
if ((a11yServiceBinder != null) && (a11yServiceInterface != null)) {
interfacesToInterrupt.add(a11yServiceInterface);
}
@@ -4962,9 +4962,14 @@
if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()
&& android.security.Flags.extendEcmToAllSettings()) {
try {
- return !mContext.getSystemService(EnhancedConfirmationManager.class)
- .isRestricted(packageName,
- AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
+ final EnhancedConfirmationManager userContextEcm =
+ mContext.createContextAsUser(UserHandle.of(userId), /* flags = */ 0)
+ .getSystemService(EnhancedConfirmationManager.class);
+ if (userContextEcm != null) {
+ return !userContextEcm.isRestricted(packageName,
+ AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
+ }
+ return false;
} catch (PackageManager.NameNotFoundException e) {
Log.e(LOG_TAG, "Exception when retrieving package:" + packageName, e);
return false;
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index 786d167..15999d1 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -166,8 +166,9 @@
if (userState.getBindInstantServiceAllowedLocked()) {
flags |= Context.BIND_ALLOW_INSTANT;
}
- if (mService == null && mContext.bindServiceAsUser(
- mIntent, this, flags, new UserHandle(userState.mUserId))) {
+ if (mClientBinder == null
+ && mContext.bindServiceAsUser(
+ mIntent, this, flags, new UserHandle(userState.mUserId))) {
userState.getBindingServicesLocked().add(mComponentName);
}
} finally {
@@ -227,20 +228,20 @@
addWindowTokensForAllDisplays();
}
synchronized (mLock) {
- if (mService != service) {
- if (mService != null) {
- mService.unlinkToDeath(this, 0);
+ if (mClientBinder != service) {
+ if (mClientBinder != null) {
+ mClientBinder.unlinkToDeath(this, 0);
}
- mService = service;
+ mClientBinder = service;
try {
- mService.linkToDeath(this, 0);
+ mClientBinder.linkToDeath(this, 0);
} catch (RemoteException re) {
Slog.e(LOG_TAG, "Failed registering death link");
binderDied();
return;
}
}
- mServiceInterface = IAccessibilityServiceClient.Stub.asInterface(service);
+ mClient = IAccessibilityServiceClient.Stub.asInterface(service);
if (userState == null) return;
userState.addServiceLocked(this);
mSystemSupport.onClientChangeLocked(false);
@@ -261,7 +262,7 @@
}
private void initializeService() {
- IAccessibilityServiceClient serviceInterface = null;
+ IAccessibilityServiceClient client = null;
synchronized (mLock) {
AccessibilityUserState userState = mUserStateWeakReference.get();
if (userState == null) return;
@@ -272,18 +273,17 @@
bindingServices.remove(mComponentName);
crashedServices.remove(mComponentName);
mAccessibilityServiceInfo.crashed = false;
- serviceInterface = mServiceInterface;
+ client = mClient;
}
// There's a chance that service is removed from enabled_accessibility_services setting
// key, but skip unbinding because of it's in binding state. Unbinds it if it's
// not in enabled service list.
- if (serviceInterface != null
- && !userState.getEnabledServicesLocked().contains(mComponentName)) {
+ if (client != null && !userState.getEnabledServicesLocked().contains(mComponentName)) {
mSystemSupport.onClientChangeLocked(false);
return;
}
}
- if (serviceInterface == null) {
+ if (client == null) {
binderDied();
return;
}
@@ -292,10 +292,9 @@
logTraceSvcClient("init",
this + "," + mId + "," + mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
}
- serviceInterface.init(this, mId, mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
+ client.init(this, mId, mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
} catch (RemoteException re) {
- Slog.w(LOG_TAG, "Error while setting connection for service: "
- + serviceInterface, re);
+ Slog.w(LOG_TAG, "Error while setting connection for service: " + client, re);
binderDied();
}
}
@@ -496,7 +495,7 @@
@Override
public boolean isCapturingFingerprintGestures() {
- return (mServiceInterface != null)
+ return (mClient != null)
&& mSecurityPolicy.canCaptureFingerprintGestures(this)
&& mCaptureFingerprintGestures;
}
@@ -506,17 +505,17 @@
if (!isCapturingFingerprintGestures()) {
return;
}
- IAccessibilityServiceClient serviceInterface;
+ IAccessibilityServiceClient client;
synchronized (mLock) {
- serviceInterface = mServiceInterface;
+ client = mClient;
}
- if (serviceInterface != null) {
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient(
"onFingerprintCapturingGesturesChanged", String.valueOf(active));
}
- mServiceInterface.onFingerprintCapturingGesturesChanged(active);
+ mClient.onFingerprintCapturingGesturesChanged(active);
} catch (RemoteException e) {
}
}
@@ -527,16 +526,16 @@
if (!isCapturingFingerprintGestures()) {
return;
}
- IAccessibilityServiceClient serviceInterface;
+ IAccessibilityServiceClient client;
synchronized (mLock) {
- serviceInterface = mServiceInterface;
+ client = mClient;
}
- if (serviceInterface != null) {
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("onFingerprintGesture", String.valueOf(gesture));
}
- mServiceInterface.onFingerprintGesture(gesture);
+ mClient.onFingerprintGesture(gesture);
} catch (RemoteException e) {
}
}
@@ -546,7 +545,7 @@
@Override
public void dispatchGesture(int sequence, ParceledListSlice gestureSteps, int displayId) {
synchronized (mLock) {
- if (mServiceInterface != null && mSecurityPolicy.canPerformGestures(this)) {
+ if (mClient != null && mSecurityPolicy.canPerformGestures(this)) {
final long identity = Binder.clearCallingIdentity();
try {
MotionEventInjector motionEventInjector =
@@ -557,16 +556,18 @@
if (motionEventInjector != null
&& mWindowManagerService.isTouchOrFaketouchDevice()) {
motionEventInjector.injectEvents(
- gestureSteps.getList(), mServiceInterface, sequence, displayId);
+ gestureSteps.getList(), mClient, sequence, displayId);
} else {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("onPerformGestureResult", sequence + ", false");
}
- mServiceInterface.onPerformGestureResult(sequence, false);
+ mClient.onPerformGestureResult(sequence, false);
} catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error sending motion event injection failure to "
- + mServiceInterface, re);
+ Slog.e(
+ LOG_TAG,
+ "Error sending motion event injection failure to " + mClient,
+ re);
}
}
} finally {
@@ -631,48 +632,47 @@
@Override
protected void createImeSessionInternal() {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (svcClientTracingEnabled()) {
logTraceSvcClient("createImeSession", "");
}
AccessibilityInputMethodSessionCallback
callback = new AccessibilityInputMethodSessionCallback(mUserId);
- listener.createImeSession(callback);
+ client.createImeSession(callback);
} catch (RemoteException re) {
- Slog.e(LOG_TAG,
- "Error requesting IME session from " + mService, re);
+ Slog.e(LOG_TAG, "Error requesting IME session from " + mClientBinder, re);
}
}
}
private void notifyMotionEventInternal(MotionEvent event) {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (mTrace.isA11yTracingEnabled()) {
logTraceSvcClient(".onMotionEvent ",
event.toString());
}
- listener.onMotionEvent(event);
+ client.onMotionEvent(event);
} catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error sending motion event to" + mService, re);
+ Slog.e(LOG_TAG, "Error sending motion event to" + mClientBinder, re);
}
}
}
private void notifyTouchStateInternal(int displayId, int state) {
- final IAccessibilityServiceClient listener = getServiceInterfaceSafely();
- if (listener != null) {
+ final IAccessibilityServiceClient client = getClientSafely();
+ if (client != null) {
try {
if (mTrace.isA11yTracingEnabled()) {
logTraceSvcClient(".onTouchStateChanged ",
TouchInteractionController.stateToString(state));
}
- listener.onTouchStateChanged(displayId, state);
+ client.onTouchStateChanged(displayId, state);
} catch (RemoteException re) {
- Slog.e(LOG_TAG, "Error sending motion event to" + mService, re);
+ Slog.e(LOG_TAG, "Error sending motion event to" + mClientBinder, re);
}
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
index 4cb3d24..cd97d83 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyAccessibilityServiceConnection.java
@@ -109,14 +109,11 @@
return mDeviceId;
}
- /**
- * Called when the proxy is registered.
- */
- void initializeServiceInterface(IAccessibilityServiceClient serviceInterface)
- throws RemoteException {
- mServiceInterface = serviceInterface;
- mService = serviceInterface.asBinder();
- mServiceInterface.init(this, mId, this.mOverlayWindowTokens.get(mDisplayId));
+ /** Called when the proxy is registered. */
+ void initializeClient(IAccessibilityServiceClient client) throws RemoteException {
+ mClient = client;
+ mClientBinder = client.asBinder();
+ mClient.init(this, mId, this.mOverlayWindowTokens.get(mDisplayId));
}
/**
diff --git a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
index b4deeb0..da11a76 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
@@ -214,7 +214,7 @@
mA11yInputFilter.disableFeaturesForDisplayIfInstalled(displayId);
}
});
- connection.initializeServiceInterface(client);
+ connection.initializeClient(client);
}
private void registerVirtualDeviceListener() {
@@ -561,8 +561,8 @@
final ProxyAccessibilityServiceConnection proxy =
mProxyA11yServiceConnections.valueAt(i);
if (proxy != null && proxy.getDeviceId() == deviceId) {
- final IBinder proxyBinder = proxy.mService;
- final IAccessibilityServiceClient proxyInterface = proxy.mServiceInterface;
+ final IBinder proxyBinder = proxy.mClientBinder;
+ final IAccessibilityServiceClient proxyInterface = proxy.mClient;
if ((proxyBinder != null) && (proxyInterface != null)) {
interfaces.add(proxyInterface);
}
diff --git a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
index f85d786..ed4eeb5 100644
--- a/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/UiAutomationManager.java
@@ -107,8 +107,7 @@
Binder.getCallingUserHandle().getIdentifier());
if (mUiAutomationService != null) {
throw new IllegalStateException(
- "UiAutomationService " + mUiAutomationService.mServiceInterface
- + "already registered!");
+ "UiAutomationService " + mUiAutomationService.mClient + "already registered!");
}
try {
@@ -130,10 +129,9 @@
mainHandler, mLock, securityPolicy, systemSupport, trace, windowManagerInternal,
systemActionPerformer, awm);
mUiAutomationServiceOwner = owner;
- mUiAutomationService.mServiceInterface = serviceClient;
+ mUiAutomationService.mClient = serviceClient;
try {
- mUiAutomationService.mServiceInterface.asBinder().linkToDeath(mUiAutomationService,
- 0);
+ mUiAutomationService.mClient.asBinder().linkToDeath(mUiAutomationService, 0);
} catch (RemoteException re) {
Slog.e(LOG_TAG, "Failed registering death link: " + re);
destroyUiAutomationService();
@@ -149,10 +147,10 @@
synchronized (mLock) {
if (useAccessibility()
&& ((mUiAutomationService == null)
- || (serviceClient == null)
- || (mUiAutomationService.mServiceInterface == null)
- || (serviceClient.asBinder()
- != mUiAutomationService.mServiceInterface.asBinder()))) {
+ || (serviceClient == null)
+ || (mUiAutomationService.mClient == null)
+ || (serviceClient.asBinder()
+ != mUiAutomationService.mClient.asBinder()))) {
throw new IllegalStateException("UiAutomationService " + serviceClient
+ " not registered!");
}
@@ -230,8 +228,7 @@
private void destroyUiAutomationService() {
synchronized (mLock) {
if (mUiAutomationService != null) {
- mUiAutomationService.mServiceInterface.asBinder().unlinkToDeath(
- mUiAutomationService, 0);
+ mUiAutomationService.mClient.asBinder().unlinkToDeath(mUiAutomationService, 0);
mUiAutomationService.onRemoved();
mUiAutomationService.resetLocked();
mUiAutomationService = null;
@@ -271,40 +268,48 @@
void connectServiceUnknownThread() {
// This needs to be done on the main thread
- mMainHandler.post(() -> {
- try {
- final IAccessibilityServiceClient serviceInterface;
- final UiAutomationService uiAutomationService;
- synchronized (mLock) {
- serviceInterface = mServiceInterface;
- uiAutomationService = mUiAutomationService;
- if (serviceInterface == null) {
- mService = null;
- } else {
- mService = mServiceInterface.asBinder();
- mService.linkToDeath(this, 0);
+ mMainHandler.post(
+ () -> {
+ try {
+ final IAccessibilityServiceClient client;
+ final UiAutomationService uiAutomationService;
+ synchronized (mLock) {
+ client = mClient;
+ uiAutomationService = mUiAutomationService;
+ if (client == null) {
+ mClientBinder = null;
+ } else {
+ mClientBinder = mClient.asBinder();
+ mClientBinder.linkToDeath(this, 0);
+ }
+ }
+ // If the client is null, the UiAutomation has been shut down on
+ // another thread.
+ if (client != null && uiAutomationService != null) {
+ uiAutomationService.addWindowTokensForAllDisplays();
+ if (mTrace.isA11yTracingEnabledForTypes(
+ AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) {
+ mTrace.logTrace(
+ "UiAutomationService.connectServiceUnknownThread",
+ AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT,
+ "serviceConnection="
+ + this
+ + ";connectionId="
+ + mId
+ + "windowToken="
+ + mOverlayWindowTokens.get(
+ Display.DEFAULT_DISPLAY));
+ }
+ client.init(
+ this,
+ mId,
+ mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
+ }
+ } catch (RemoteException re) {
+ Slog.w(LOG_TAG, "Error initializing connection", re);
+ destroyUiAutomationService();
}
- }
- // If the serviceInterface is null, the UiAutomation has been shut down on
- // another thread.
- if (serviceInterface != null && uiAutomationService != null) {
- uiAutomationService.addWindowTokensForAllDisplays();
- if (mTrace.isA11yTracingEnabledForTypes(
- AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) {
- mTrace.logTrace("UiAutomationService.connectServiceUnknownThread",
- AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT,
- "serviceConnection=" + this + ";connectionId=" + mId
- + "windowToken="
- + mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
- }
- serviceInterface.init(this, mId,
- mOverlayWindowTokens.get(Display.DEFAULT_DISPLAY));
- }
- } catch (RemoteException re) {
- Slog.w(LOG_TAG, "Error initializing connection", re);
- destroyUiAutomationService();
- }
- });
+ });
}
@Override
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 1e723b5..c8f8c2a 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -204,7 +204,8 @@
serviceIntent,
targetUser,
safeExecuteAppFunctionCallback,
- /* bindFlags= */ Context.BIND_AUTO_CREATE);
+ /* bindFlags= */ Context.BIND_AUTO_CREATE
+ | Context.BIND_FOREGROUND_SERVICE);
})
.exceptionally(
ex -> {
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index b2c679f..f6ac706 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -1444,7 +1444,7 @@
* in {@link #allocateAppWidgetId}.
*
* @param callingPackage The package that calls this method.
- * @param appWidgetId The id of theapp widget to bind.
+ * @param appWidgetId The id of the widget to bind.
* @param providerProfileId The user/profile id of the provider.
* @param providerComponent The {@link ComponentName} that provides the widget.
* @param options The options to pass to the provider.
@@ -1738,6 +1738,14 @@
return false;
}
+ /**
+ * Called by a {@link AppWidgetHost} to remove all records (i.e. {@link Host}
+ * and all {@link Widget} associated with the host) from a specified host.
+ *
+ * @param callingPackage The package that calls this method.
+ * @param hostId id of the {@link Host}.
+ * @see AppWidgetHost#deleteHost()
+ */
@Override
public void deleteHost(String callingPackage, int hostId) {
final int userId = UserHandle.getCallingUserId();
@@ -1771,6 +1779,15 @@
}
}
+ /**
+ * Called by a host process to remove all records (i.e. {@link Host}
+ * and all {@link Widget} associated with the host) from all hosts associated
+ * with the calling process.
+ *
+ * Typically used in clean up after test execution.
+ *
+ * @see AppWidgetHost#deleteAllHosts()
+ */
@Override
public void deleteAllHosts() {
final int userId = UserHandle.getCallingUserId();
@@ -1805,6 +1822,18 @@
}
}
+ /**
+ * Returns the {@link AppWidgetProviderInfo} for the specified AppWidget.
+ *
+ * Typically used by launcher during the restore of an AppWidget, the binding
+ * of new AppWidget, and during grid size migration.
+ *
+ * @param callingPackage The package that calls this method.
+ * @param appWidgetId Id of the widget.
+ * @return The {@link AppWidgetProviderInfo} for the specified widget.
+ *
+ * @see AppWidgetManager#getAppWidgetInfo(int)
+ */
@Override
public AppWidgetProviderInfo getAppWidgetInfo(String callingPackage, int appWidgetId) {
final int userId = UserHandle.getCallingUserId();
@@ -1859,6 +1888,17 @@
}
}
+ /**
+ * Returns the most recent {@link RemoteViews} of the specified AppWidget.
+ * Typically serves as a cache of the content of the AppWidget.
+ *
+ * @param callingPackage The package that calls this method.
+ * @param appWidgetId Id of the widget.
+ * @return The {@link RemoteViews} of the specified widget.
+ *
+ * @see AppWidgetHost#updateAppWidgetDeferred(String, int)
+ * @see AppWidgetHost#setListener(int, AppWidgetHostListener)
+ */
@Override
public RemoteViews getAppWidgetViews(String callingPackage, int appWidgetId) {
final int userId = UserHandle.getCallingUserId();
@@ -1886,6 +1926,29 @@
}
}
+ /**
+ * Update the extras for a given widget instance.
+ * <p>
+ * The extras can be used to embed additional information about this widget to be accessed
+ * by the associated widget's AppWidgetProvider.
+ *
+ * <p>
+ * The new options are merged into existing options using {@link Bundle#putAll} semantics.
+ *
+ * <p>
+ * Typically called by a {@link AppWidgetHost} (e.g. Launcher) to notify
+ * {@link AppWidgetProvider} regarding contextual changes (e.g. sizes) when rendering the
+ * widget.
+ * Calling this method would trigger onAppWidgetOptionsChanged() callback on the provider's
+ * side.
+ *
+ * @param callingPackage The package that calls this method.
+ * @param appWidgetId Id of the widget.
+ * @param options New options associate with this widget.
+ *
+ * @see AppWidgetManager#getAppWidgetOptions(int, Bundle)
+ * @see AppWidgetProvider#onAppWidgetOptionsChanged(Context, AppWidgetManager, int, Bundle)
+ */
@Override
public void updateAppWidgetOptions(String callingPackage, int appWidgetId, Bundle options) {
final int userId = UserHandle.getCallingUserId();
@@ -1919,6 +1982,21 @@
}
}
+ /**
+ * Get the extras associated with a given widget instance.
+ * <p>
+ * The extras can be used to embed additional information about this widget to be accessed
+ * by the associated widget's AppWidgetProvider.
+ *
+ * Typically called by a host process (e.g. Launcher) to determine if they need to update the
+ * options of the widget.
+ *
+ * @see #updateAppWidgetOptions(String, int, Bundle)
+ *
+ * @param callingPackage The package that calls this method.
+ * @param appWidgetId Id of the widget.
+ * @return The options associated with the specified widget instance.
+ */
@Override
public Bundle getAppWidgetOptions(String callingPackage, int appWidgetId) {
final int userId = UserHandle.getCallingUserId();
@@ -1946,6 +2024,28 @@
}
}
+ /**
+ * Updates the content of the widgets (as specified by appWidgetIds) using the provided
+ * {@link RemoteViews}.
+ *
+ * Typically called by the provider's process. Either in response to the invocation of
+ * {@link AppWidgetProvider#onUpdate} or upon receiving the
+ * {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} broadcast.
+ *
+ * <p>
+ * Note that the RemoteViews parameter will be cached by the AppWidgetService, and hence should
+ * contain a complete representation of the widget. For performing partial widget updates, see
+ * {@link #partiallyUpdateAppWidgetIds(String, int[], RemoteViews)}.
+ *
+ * @param callingPackage The package that calls this method.
+ * @param appWidgetIds Ids of the widgets to be updated.
+ * @param views The RemoteViews object containing the update.
+ *
+ * @see AppWidgetProvider#onUpdate(Context, AppWidgetManager, int[])
+ * @see AppWidgetManager#ACTION_APPWIDGET_UPDATE
+ * @see AppWidgetManager#updateAppWidget(int, RemoteViews)
+ * @see AppWidgetManager#updateAppWidget(int[], RemoteViews)
+ */
@Override
public void updateAppWidgetIds(String callingPackage, int[] appWidgetIds,
RemoteViews views) {
@@ -1956,6 +2056,27 @@
updateAppWidgetIds(callingPackage, appWidgetIds, views, false);
}
+ /**
+ * Perform an incremental update or command on the widget(s) specified by appWidgetIds.
+ * <p>
+ * This update differs from {@link #updateAppWidgetIds(int[], RemoteViews)} in that the
+ * RemoteViews object which is passed is understood to be an incomplete representation of the
+ * widget, and hence does not replace the cached representation of the widget. As of API
+ * level 17, the new properties set within the views objects will be appended to the cached
+ * representation of the widget, and hence will persist.
+ *
+ * <p>
+ * This method will be ignored if a widget has not received a full update via
+ * {@link #updateAppWidget(int[], RemoteViews)}.
+ *
+ * @param callingPackage The package that calls this method.
+ * @param appWidgetIds Ids of the widgets to be updated.
+ * @param views The RemoteViews object containing the incremental update / command.
+ *
+ * @see AppWidgetManager#partiallyUpdateAppWidget(int[], RemoteViews)
+ * @see RemoteViews#setDisplayedChild(int, int)
+ * @see RemoteViews#setScrollPosition(int, int)
+ */
@Override
public void partiallyUpdateAppWidgetIds(String callingPackage, int[] appWidgetIds,
RemoteViews views) {
@@ -1966,6 +2087,24 @@
updateAppWidgetIds(callingPackage, appWidgetIds, views, true);
}
+ /**
+ * Callback function which marks specified providers as extended from AppWidgetProvider.
+ *
+ * This information is used to determine if the system can combine
+ * {@link AppWidgetManager#ACTION_APPWIDGET_ENABLED} and
+ * {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} into a single broadcast.
+ *
+ * Note: The system can only combine the two broadcasts if the provider is extended from
+ * AppWidgetProvider. When they do, they are expected to override the
+ * {@link AppWidgetProvider#onUpdate} callback function to provide updates, as opposed to
+ * listening for {@link AppWidgetManager#ACTION_APPWIDGET_UPDATE} broadcasts directly.
+ *
+ * @see AppWidgetManager#ACTION_APPWIDGET_ENABLED
+ * @see AppWidgetManager#ACTION_APPWIDGET_UPDATE
+ * @see AppWidgetManager#ACTION_APPWIDGET_ENABLE_AND_UPDATE
+ * @see AppWidgetProvider#onReceive(Context, Intent)
+ * @see #sendEnableAndUpdateIntentLocked
+ */
@Override
public void notifyProviderInheritance(@Nullable final ComponentName[] componentNames) {
final int userId = UserHandle.getCallingUserId();
@@ -2000,6 +2139,15 @@
}
}
+ /**
+ * Notifies the specified collection view in all the specified AppWidget instances
+ * to invalidate their data.
+ *
+ * This method is effectively deprecated since
+ * {@link RemoteViews#setRemoteAdapter(int, Intent)} has been deprecated.
+ *
+ * @see AppWidgetManager#notifyAppWidgetViewDataChanged(int[], int)
+ */
@Override
public void notifyAppWidgetViewDataChanged(String callingPackage, int[] appWidgetIds,
int viewId) {
@@ -2035,6 +2183,18 @@
}
}
+ /**
+ * Updates the content of all widgets associated with given provider (as specified by
+ * componentName) using the provided {@link RemoteViews}.
+ *
+ * Typically called by the provider's process when there's an update that needs to be supplied
+ * to all instances of the widgets.
+ *
+ * @param componentName The component name of the provider.
+ * @param views The RemoteViews object containing the update.
+ *
+ * @see AppWidgetManager#updateAppWidget(ComponentName, RemoteViews)
+ */
@Override
public void updateAppWidgetProvider(ComponentName componentName, RemoteViews views) {
final int userId = UserHandle.getCallingUserId();
@@ -2068,6 +2228,27 @@
}
}
+ /**
+ * Updates the info for the supplied AppWidget provider. Apps can use this to change the default
+ * behavior of the widget based on the state of the app (e.g., if the user is logged in
+ * or not). Calling this API completely replaces the previous definition.
+ *
+ * <p>
+ * The manifest entry of the provider should contain an additional meta-data tag similar to
+ * {@link AppWidgetManager#META_DATA_APPWIDGET_PROVIDER} which should point to any alternative
+ * definitions for the provider.
+ *
+ * <p>
+ * This is persisted across device reboots and app updates. If this meta-data key is not
+ * present in the manifest entry, the info reverts to default.
+ *
+ * @param provider {@link ComponentName} for the {@link
+ * android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget.
+ * @param metaDataKey key for the meta-data tag pointing to the new provider info. Use null
+ * to reset any previously set info.
+ *
+ * @see AppWidgetManager#updateAppWidgetProviderInfo(ComponentName, String)
+ */
@Override
public void updateAppWidgetProviderInfo(ComponentName componentName, String metadataKey) {
final int userId = UserHandle.getCallingUserId();
@@ -2119,6 +2300,11 @@
}
}
+ /**
+ * Returns true if the default launcher app on the device (the one that currently
+ * holds the android.app.role.HOME role) can support pinning widgets
+ * (typically means adding widgets into home screen).
+ */
@Override
public boolean isRequestPinAppWidgetSupported() {
synchronized (mLock) {
@@ -2133,6 +2319,44 @@
LauncherApps.PinItemRequest.REQUEST_TYPE_APPWIDGET);
}
+ /**
+ * Request to pin an app widget on the current launcher. It's up to the launcher to accept this
+ * request (optionally showing a user confirmation). If the request is accepted, the caller will
+ * get a confirmation with extra {@link #EXTRA_APPWIDGET_ID}.
+ *
+ * <p>When a request is denied by the user, the caller app will not get any response.
+ *
+ * <p>Only apps with a foreground activity or a foreground service can call it. Otherwise
+ * it'll throw {@link IllegalStateException}.
+ *
+ * <p>It's up to the launcher how to handle previous pending requests when the same package
+ * calls this API multiple times in a row. It may ignore the previous requests,
+ * for example.
+ *
+ * <p>Launcher will not show the configuration activity associated with the provider in this
+ * case. The app could either show the configuration activity as a response to the callback,
+ * or show if before calling the API (various configurations can be encapsulated in
+ * {@code successCallback} to avoid persisting them before the widgetId is known).
+ *
+ * @param provider The {@link ComponentName} for the {@link
+ * android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget.
+ * @param extras If not null, this is passed to the launcher app. For eg {@link
+ * #EXTRA_APPWIDGET_PREVIEW} can be used for a custom preview.
+ * @param successCallback If not null, this intent will be sent when the widget is created.
+ *
+ * @return {@code TRUE} if the launcher supports this feature. Note the API will return without
+ * waiting for the user to respond, so getting {@code TRUE} from this API does *not* mean
+ * the shortcut is pinned. {@code FALSE} if the launcher doesn't support this feature or if
+ * calling app belongs to a user-profile with items restricted on home screen.
+ *
+ * @see android.content.pm.ShortcutManager#isRequestPinShortcutSupported()
+ * @see android.content.pm.ShortcutManager#requestPinShortcut(ShortcutInfo, IntentSender)
+ * @see AppWidgetManager#isRequestPinAppWidgetSupported()
+ * @see AppWidgetManager#requestPinAppWidget(ComponentName, Bundle, PendingIntent)
+ *
+ * @throws IllegalStateException The caller doesn't have a foreground activity or a foreground
+ * service or when the user is locked.
+ */
@Override
public boolean requestPinAppWidget(String callingPackage, ComponentName componentName,
Bundle extras, IntentSender resultSender) {
@@ -2184,6 +2408,24 @@
callingPid, callingUid) == PackageManager.PERMISSION_GRANTED;
}
+ /**
+ * Gets the AppWidget providers for the given user profile. User profile can only
+ * be the current user or a profile of the current user. For example, the current
+ * user may have a corporate profile. In this case the parent user profile has a
+ * child profile, the corporate one.
+ *
+ * @param categoryFilter Will only return providers which register as any of the specified
+ * specified categories. See {@link AppWidgetProviderInfo#widgetCategory}.
+ * @param profile A profile of the current user which to be queried. The user
+ * is itself also a profile. If null, the providers only for the current user
+ * are returned.
+ * @param packageName If specified, will only return providers from the given package.
+ * @return The installed providers.
+ *
+ * @see android.os.Process#myUserHandle()
+ * @see android.os.UserManager#getUserProfiles()
+ * @see AppWidgetManager#getInstalledProvidersForProfile(int, UserHandle, String)
+ */
@Override
public ParceledListSlice<AppWidgetProviderInfo> getInstalledProvidersForProfile(int categoryFilter,
int profileId, String packageName) {
@@ -2244,6 +2486,26 @@
}
}
+ /**
+ * Updates the content of the widgets (as specified by appWidgetIds) using the provided
+ * {@link RemoteViews}.
+ *
+ * If performing a partial update, the given RemoteViews object is merged into existing
+ * RemoteViews object.
+ *
+ * Fails silently if appWidgetIds is null or empty, or cannot found a widget with the given
+ * appWidgetId.
+ *
+ * @param callingPackage The package that calls this method.
+ * @param appWidgetIds Ids of the widgets to be updated.
+ * @param views The RemoteViews object containing the update.
+ * @param partially Whether it was a partial update.
+ *
+ * @see AppWidgetProvider#onUpdate(Context, AppWidgetManager, int[])
+ * @see AppWidgetManager#ACTION_APPWIDGET_UPDATE
+ * @see AppWidgetManager#updateAppWidget(int, RemoteViews)
+ * @see AppWidgetManager#updateAppWidget(int[], RemoteViews)
+ */
private void updateAppWidgetIds(String callingPackage, int[] appWidgetIds,
RemoteViews views, boolean partially) {
final int userId = UserHandle.getCallingUserId();
@@ -2273,12 +2535,29 @@
}
}
+ /**
+ * Increment the counter of widget ids and return the new id.
+ *
+ * Typically called by {@link #allocateAppWidgetId} when a instance of widget is created,
+ * either as a result of being pinned by launcher or added during a restore.
+ *
+ * Note: A widget id is a monotonically increasing integer that uniquely identifies the widget
+ * instance.
+ *
+ * TODO: Revisit this method and determine whether we need to alter the widget id during
+ * the restore since widget id mismatch potentially leads to some issues in the past.
+ */
private int incrementAndGetAppWidgetIdLocked(int userId) {
final int appWidgetId = peekNextAppWidgetIdLocked(userId) + 1;
mNextAppWidgetIds.put(userId, appWidgetId);
return appWidgetId;
}
+ /**
+ * Called by {@link #readProfileStateFromFileLocked} when widgets/providers/hosts are loaded
+ * from disk, which ensures mNextAppWidgetIds is larger than any existing widget id for given
+ * user.
+ */
private void setMinAppWidgetIdLocked(int userId, int minWidgetId) {
final int nextAppWidgetId = peekNextAppWidgetIdLocked(userId);
if (nextAppWidgetId < minWidgetId) {
diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java
index 0713999..60b826b 100644
--- a/services/core/java/android/os/BatteryStatsInternal.java
+++ b/services/core/java/android/os/BatteryStatsInternal.java
@@ -41,6 +41,7 @@
public static final int CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER = 3;
public static final int CPU_WAKEUP_SUBSYSTEM_SENSOR = 4;
public static final int CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA = 5;
+ public static final int CPU_WAKEUP_SUBSYSTEM_BLUETOOTH = 6;
/** @hide */
@IntDef(prefix = {"CPU_WAKEUP_SUBSYSTEM_"}, value = {
@@ -50,6 +51,7 @@
CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER,
CPU_WAKEUP_SUBSYSTEM_SENSOR,
CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA,
+ CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CpuWakeupSubsystem {
@@ -99,6 +101,14 @@
public abstract void noteCpuWakingNetworkPacket(Network network, long elapsedMillis, int uid);
/**
+ * Informs battery stats of a sysproxy packet that woke up the CPU
+ *
+ * @param uid The uid that received the packet.
+ * @param elapsedMillis The time of the packet's arrival in elapsed timebase.
+ */
+ public abstract void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis);
+
+ /**
* Informs battery stats of binder stats for the given work source UID.
*/
public abstract void noteBinderCallStats(int workSourceUid, long incrementalBinderCallCount,
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index d86bae1..e64a480 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -219,7 +219,7 @@
public static final int FAILED_MOUNT_RESET_TIMEOUT_SECONDS = 10;
/** Extended timeout for the system server watchdog. */
- private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 20 * 1000;
+ private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 30 * 1000;
/** Extended timeout for the system server watchdog for vold#partition operation. */
private static final int PARTITION_OPERATION_WATCHDOG_TIMEOUT_MS = 3 * 60 * 1000;
@@ -3251,7 +3251,7 @@
if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) {
throw new SecurityException("no permission to commit checkpoint changes");
}
-
+ extendWatchdogTimeout("vold#commitChanges might be slow");
mVold.commitChanges();
}
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 89dfc73..12e8c57 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -380,10 +380,12 @@
}
/** Gets transports that need to be marked as restricted by the VCN from CarrierConfig */
+ // TODO: b/262269892 This method was created to perform experiments before the relevant API
+ // was exposed. Now it is obsolete and should be removed.
@VisibleForTesting(visibility = Visibility.PRIVATE)
public Set<Integer> getRestrictedTransportsFromCarrierConfig(
ParcelUuid subGrp, TelephonySubscriptionSnapshot lastSnapshot) {
- if (!Build.IS_ENG && !Build.IS_USERDEBUG) {
+ if (!Build.isDebuggable()) {
return RESTRICTED_TRANSPORTS_DEFAULT;
}
diff --git a/services/core/java/com/android/server/WiredAccessoryManager.java b/services/core/java/com/android/server/WiredAccessoryManager.java
index b271d7e..ab69cd1 100644
--- a/services/core/java/com/android/server/WiredAccessoryManager.java
+++ b/services/core/java/com/android/server/WiredAccessoryManager.java
@@ -118,8 +118,11 @@
if (mInputManager.getSwitchState(-1, InputDevice.SOURCE_ANY, SW_LINEOUT_INSERT) == 1) {
switchValues |= SW_LINEOUT_INSERT_BIT;
}
- notifyWiredAccessoryChanged(0, switchValues,
- SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT);
+ notifyWiredAccessoryChanged(
+ 0,
+ switchValues,
+ SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT,
+ true /*isSynchronous*/);
}
@@ -135,7 +138,13 @@
}
@Override
- public void notifyWiredAccessoryChanged(long whenNanos, int switchValues, int switchMask) {
+ public void notifyWiredAccessoryChanged(
+ long whenNanos, int switchValues, int switchMask) {
+ notifyWiredAccessoryChanged(whenNanos, switchValues, switchMask, false /*isSynchronous*/);
+ }
+
+ public void notifyWiredAccessoryChanged(
+ long whenNanos, int switchValues, int switchMask, boolean isSynchronous) {
if (LOG) {
Slog.v(TAG, "notifyWiredAccessoryChanged: when=" + whenNanos
+ " bits=" + switchCodeToString(switchValues, switchMask)
@@ -172,8 +181,10 @@
break;
}
- updateLocked(NAME_H2W,
- (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset);
+ updateLocked(
+ NAME_H2W,
+ (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset,
+ isSynchronous);
}
}
@@ -195,8 +206,9 @@
*
* @param newName One of the NAME_xxx variables defined above.
* @param newState 0 or one of the BIT_xxx variables defined above.
+ * @param isSynchronous boolean to determine whether should happen sync or async
*/
- private void updateLocked(String newName, int newState) {
+ private void updateLocked(String newName, int newState, boolean isSynchronous) {
// Retain only relevant bits
int headsetState = newState & SUPPORTED_HEADSETS;
int usb_headset_anlg = headsetState & BIT_USB_HEADSET_ANLG;
@@ -234,12 +246,15 @@
return;
}
- mWakeLock.acquire();
-
- Log.i(TAG, "MSG_NEW_DEVICE_STATE");
- Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState,
- mHeadsetState, "");
- mHandler.sendMessage(msg);
+ if (isSynchronous) {
+ setDevicesState(headsetState, mHeadsetState, "");
+ } else {
+ mWakeLock.acquire();
+ Log.i(TAG, "MSG_NEW_DEVICE_STATE");
+ Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState,
+ mHeadsetState, "");
+ mHandler.sendMessage(msg);
+ }
mHeadsetState = headsetState;
}
@@ -439,7 +454,10 @@
for (int i = 0; i < mUEventInfo.size(); ++i) {
UEventInfo uei = mUEventInfo.get(i);
if (devPath.equals(uei.getDevPath())) {
- updateLocked(name, uei.computeNewHeadsetState(mHeadsetState, state));
+ updateLocked(
+ name,
+ uei.computeNewHeadsetState(mHeadsetState, state),
+ false /*isSynchronous*/);
return;
}
}
@@ -550,7 +568,10 @@
synchronized (mLock) {
int mask = maskAndState.first;
int state = maskAndState.second;
- updateLocked(name, mHeadsetState & ~(mask & ~state) | (mask & state));
+ updateLocked(
+ name,
+ mHeadsetState & ~(mask & ~state) | (mask & state),
+ false /*isSynchronous*/);
return;
}
}
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index 88edb12..3499a3a 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -1773,8 +1773,7 @@
// Create a Session for the target user and pass in the bundle
completeCloningAccount(response, result, account, toAccounts, userFrom);
} else {
- // Bundle format is not defined.
- super.onResultSkipSanitization(result);
+ super.onResult(result);
}
}
}.bind();
@@ -1861,8 +1860,7 @@
// account to avoid retries?
// TODO: what we do with the visibility?
- // Bundle format is not defined.
- super.onResultSkipSanitization(result);
+ super.onResult(result);
}
@Override
@@ -2108,7 +2106,6 @@
@Override
public void onResult(Bundle result) {
Bundle.setDefusable(result, true);
- result = sanitizeBundle(result);
IAccountManagerResponse response = getResponseAndClose();
if (response != null) {
try {
@@ -2462,7 +2459,6 @@
@Override
public void onResult(Bundle result) {
Bundle.setDefusable(result, true);
- result = sanitizeBundle(result);
if (result != null && result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
&& !result.containsKey(AccountManager.KEY_INTENT)) {
final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
@@ -2977,7 +2973,6 @@
@Override
public void onResult(Bundle result) {
Bundle.setDefusable(result, true);
- result = sanitizeBundle(result);
if (result != null) {
String label = result.getString(AccountManager.KEY_AUTH_TOKEN_LABEL);
Bundle bundle = new Bundle();
@@ -3155,7 +3150,6 @@
@Override
public void onResult(Bundle result) {
Bundle.setDefusable(result, true);
- result = sanitizeBundle(result);
if (result != null) {
if (result.containsKey(AccountManager.KEY_AUTH_TOKEN_LABEL)) {
Intent intent = newGrantCredentialsPermissionIntent(
@@ -3627,12 +3621,6 @@
@Override
public void onResult(Bundle result) {
Bundle.setDefusable(result, true);
- Bundle sessionBundle = null;
- if (result != null) {
- // Session bundle will be removed from result.
- sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE);
- }
- result = sanitizeBundle(result);
mNumResults++;
Intent intent = null;
if (result != null) {
@@ -3694,6 +3682,7 @@
// bundle contains data necessary for finishing the session
// later. The session bundle will be encrypted here and
// decrypted later when trying to finish the session.
+ Bundle sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE);
if (sessionBundle != null) {
String accountType = sessionBundle.getString(AccountManager.KEY_ACCOUNT_TYPE);
if (TextUtils.isEmpty(accountType)
@@ -4081,7 +4070,6 @@
@Override
public void onResult(Bundle result) {
Bundle.setDefusable(result, true);
- result = sanitizeBundle(result);
IAccountManagerResponse response = getResponseAndClose();
if (response == null) {
return;
@@ -4395,7 +4383,6 @@
@Override
public void onResult(Bundle result) {
Bundle.setDefusable(result, true);
- result = sanitizeBundle(result);
mNumResults++;
if (result == null) {
onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle");
@@ -4952,68 +4939,6 @@
callback, resultReceiver);
}
-
- // All keys for Strings passed from AbstractAccountAuthenticator using Bundle.
- private static final String[] sStringBundleKeys = new String[] {
- AccountManager.KEY_ACCOUNT_NAME,
- AccountManager.KEY_ACCOUNT_TYPE,
- AccountManager.KEY_AUTHTOKEN,
- AccountManager.KEY_AUTH_TOKEN_LABEL,
- AccountManager.KEY_ERROR_MESSAGE,
- AccountManager.KEY_PASSWORD,
- AccountManager.KEY_ACCOUNT_STATUS_TOKEN};
-
- /**
- * Keep only documented fields in a Bundle received from AbstractAccountAuthenticator.
- */
- protected static Bundle sanitizeBundle(Bundle bundle) {
- if (bundle == null) {
- return null;
- }
- Bundle sanitizedBundle = new Bundle();
- Bundle.setDefusable(sanitizedBundle, true);
- int updatedKeysCount = 0;
- for (String stringKey : sStringBundleKeys) {
- if (bundle.containsKey(stringKey)) {
- String value = bundle.getString(stringKey);
- sanitizedBundle.putString(stringKey, value);
- updatedKeysCount++;
- }
- }
- String key = AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY;
- if (bundle.containsKey(key)) {
- long expiryMillis = bundle.getLong(key, 0L);
- sanitizedBundle.putLong(key, expiryMillis);
- updatedKeysCount++;
- }
- key = AccountManager.KEY_BOOLEAN_RESULT;
- if (bundle.containsKey(key)) {
- boolean booleanResult = bundle.getBoolean(key, false);
- sanitizedBundle.putBoolean(key, booleanResult);
- updatedKeysCount++;
- }
- key = AccountManager.KEY_ERROR_CODE;
- if (bundle.containsKey(key)) {
- int errorCode = bundle.getInt(key, 0);
- sanitizedBundle.putInt(key, errorCode);
- updatedKeysCount++;
- }
- key = AccountManager.KEY_INTENT;
- if (bundle.containsKey(key)) {
- Intent intent = bundle.getParcelable(key, Intent.class);
- sanitizedBundle.putParcelable(key, intent);
- updatedKeysCount++;
- }
- if (bundle.containsKey(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE)) {
- // The field is not copied in sanitized bundle.
- updatedKeysCount++;
- }
- if (updatedKeysCount != bundle.size()) {
- Log.w(TAG, "Size mismatch after sanitizeBundle call.");
- }
- return sanitizedBundle;
- }
-
private abstract class Session extends IAccountAuthenticatorResponse.Stub
implements IBinder.DeathRecipient, ServiceConnection {
private final Object mSessionLock = new Object();
@@ -5304,15 +5229,10 @@
}
}
}
+
@Override
public void onResult(Bundle result) {
Bundle.setDefusable(result, true);
- result = sanitizeBundle(result);
- onResultSkipSanitization(result);
- }
-
- public void onResultSkipSanitization(Bundle result) {
- Bundle.setDefusable(result, true);
mNumResults++;
Intent intent = null;
if (result != null) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a21a3f1..35323d6 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -579,7 +579,7 @@
static final int RESERVED_BYTES_PER_LOGCAT_LINE = 100;
// How many seconds should the system wait before terminating the spawned logcat process.
- static final int LOGCAT_TIMEOUT_SEC = 10;
+ static final int LOGCAT_TIMEOUT_SEC = Flags.logcatLongerTimeout() ? 15 : 10;
// Necessary ApplicationInfo flags to mark an app as persistent
static final int PERSISTENT_MASK =
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 75e9fad..ef82c74 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -437,7 +437,6 @@
mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context,
mPowerAttributor, mPowerProfile, mCpuScalingPolicies,
mPowerStatsStore, Clock.SYSTEM_CLOCK);
- mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore);
mDumpHelper = new BatteryStatsDumpHelperImpl(mBatteryUsageStatsProvider);
mCpuWakeupStats = new CpuWakeupStats(context, R.xml.irq_device_map, mHandler);
mConfigFile = new AtomicFile(new File(systemDir, "battery_usage_stats_config"));
@@ -504,6 +503,9 @@
}
public void systemServicesReady() {
+ mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore,
+ Flags.accumulateBatteryUsageStats());
+
MultiStatePowerAttributor attributor = (MultiStatePowerAttributor) mPowerAttributor;
mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CPU,
Flags.streamlinedBatteryStats());
@@ -662,6 +664,8 @@
} else if (nc.hasTransport(TRANSPORT_CELLULAR)) {
return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
}
+ // For TRANSPORT_BLUETOOTH, we have a separate channel to catch Bluetooth wakeups.
+ // See noteCpuWakingSysproxyPacket method.
return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
}
@@ -684,6 +688,15 @@
}
@Override
+ public void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis) {
+ if (uid < 0) {
+ Slog.e(TAG, "Invalid uid for waking bluetooth proxy packet: " + uid);
+ return;
+ }
+ noteCpuWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, elapsedMillis, uid);
+ }
+
+ @Override
public void noteBinderCallStats(int workSourceUid, long incrementatCallCount,
Collection<BinderCallsStats.CallStat> callStats) {
synchronized (BatteryStatsService.this.mLock) {
@@ -1100,14 +1113,17 @@
DEVICE_CONFIG_NAMESPACE,
MIN_CONSUMED_POWER_THRESHOLD_KEY,
0);
- final BatteryUsageStatsQuery query =
- new BatteryUsageStatsQuery.Builder()
- .setMaxStatsAgeMs(0)
- .includeProcessStateData()
- .includeVirtualUids()
- .setMinConsumedPowerThreshold(minConsumedPowerThreshold)
- .build();
- bus = getBatteryUsageStats(List.of(query)).get(0);
+ BatteryUsageStatsQuery.Builder query = new BatteryUsageStatsQuery.Builder()
+ .setMaxStatsAgeMs(0)
+ .includeProcessStateData()
+ .includeVirtualUids()
+ .setMinConsumedPowerThreshold(minConsumedPowerThreshold);
+
+ if (Flags.accumulateBatteryUsageStats()) {
+ query.accumulated();
+ }
+
+ bus = getBatteryUsageStats(List.of(query.build())).get(0);
final int pullResult =
new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data);
try {
@@ -3016,6 +3032,9 @@
if (Flags.streamlinedBatteryStats()) {
pw.println(" --sample: collect and dump a sample of stats for debugging purpose");
}
+ if (Flags.accumulateBatteryUsageStats()) {
+ pw.println(" --accumulated: continuously accumulated since setup or reset-all");
+ }
pw.println(" <package.name>: optional name of package to filter output by.");
pw.println(" -h: print this help text.");
pw.println("Battery stats (batterystats) commands:");
@@ -3083,7 +3102,7 @@
}
private void dumpUsageStats(FileDescriptor fd, PrintWriter pw, int model,
- boolean proto) {
+ boolean proto, boolean accumulated) {
awaitCompletion();
syncStats("dump", BatteryExternalStatsWorker.UPDATE_ALL);
@@ -3097,6 +3116,9 @@
if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
builder.powerProfileModeledOnly();
}
+ if (accumulated) {
+ builder.accumulated();
+ }
BatteryUsageStatsQuery query = builder.build();
synchronized (mStats) {
mStats.prepareForDumpLocked();
@@ -3287,6 +3309,7 @@
} else if ("--usage".equals(arg)) {
int model = BatteryConsumer.POWER_MODEL_UNDEFINED;
boolean proto = false;
+ boolean accumulated = false;
for (int j = i + 1; j < args.length; j++) {
switch (args[j]) {
case "--proto":
@@ -3309,9 +3332,12 @@
}
break;
}
+ case "--accumulated":
+ accumulated = true;
+ break;
}
}
- dumpUsageStats(fd, pw, model, proto);
+ dumpUsageStats(fd, pw, model, proto, accumulated);
return;
} else if ("--wakeups".equals(arg)) {
mCpuWakeupStats.dump(new IndentingPrintWriter(pw, " "),
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index 3334393..9b51b6a 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -194,4 +194,15 @@
metadata {
purpose: PURPOSE_BUGFIX
}
+}
+
+flag {
+ name: "logcat_longer_timeout"
+ namespace: "backstage_power"
+ description: "Wait longer during the logcat gathering operation"
+ bug: "292533246"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index a6389f7..e0cf96f 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -1031,6 +1031,9 @@
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
+ if (action == null) {
+ return;
+ }
String pkgName = intent.getData().getEncodedSchemeSpecificPart().intern();
int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID);
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 0fd22c5..87504154 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1321,9 +1321,9 @@
sendLMsgNoDelay(MSG_II_SET_LE_AUDIO_OUT_VOLUME, SENDMSG_REPLACE, info);
}
- /*package*/ void postSetModeOwner(int mode, int pid, int uid) {
- sendLMsgNoDelay(MSG_I_SET_MODE_OWNER, SENDMSG_REPLACE,
- new AudioModeInfo(mode, pid, uid));
+ /*package*/ void postSetModeOwner(int mode, int pid, int uid, boolean signal) {
+ sendILMsgNoDelay(MSG_IL_SET_MODE_OWNER, SENDMSG_REPLACE,
+ signal ? 1 : 0, new AudioModeInfo(mode, pid, uid));
}
/*package*/ void postBluetoothDeviceConfigChange(@NonNull BtDeviceInfo info) {
@@ -1564,38 +1564,6 @@
sendLMsgNoDelay(MSG_L_COMMUNICATION_ROUTE_CLIENT_DIED, SENDMSG_QUEUE, client);
}
- /*package*/ void postSaveSetPreferredDevicesForStrategy(int strategy,
- List<AudioDeviceAttributes> devices)
- {
- sendILMsgNoDelay(MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_QUEUE, strategy, devices);
- }
-
- /*package*/ void postSaveRemovePreferredDevicesForStrategy(int strategy) {
- sendIMsgNoDelay(MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_QUEUE, strategy);
- }
-
- /*package*/ void postSaveSetDeviceAsNonDefaultForStrategy(
- int strategy, AudioDeviceAttributes device) {
- sendILMsgNoDelay(MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
- }
-
- /*package*/ void postSaveRemoveDeviceAsNonDefaultForStrategy(
- int strategy, AudioDeviceAttributes device) {
- sendILMsgNoDelay(
- MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device);
- }
-
- /*package*/ void postSaveSetPreferredDevicesForCapturePreset(
- int capturePreset, List<AudioDeviceAttributes> devices) {
- sendILMsgNoDelay(
- MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET, SENDMSG_QUEUE, capturePreset, devices);
- }
-
- /*package*/ void postSaveClearPreferredDevicesForCapturePreset(int capturePreset) {
- sendIMsgNoDelay(
- MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET, SENDMSG_QUEUE, capturePreset);
- }
-
/*package*/ void postUpdateCommunicationRouteClient(
int btScoRequesterUid, String eventSource) {
sendILMsgNoDelay(MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE,
@@ -2025,7 +1993,7 @@
mBtHelper.setAvrcpAbsoluteVolumeIndex(msg.arg1);
}
break;
- case MSG_I_SET_MODE_OWNER:
+ case MSG_IL_SET_MODE_OWNER:
synchronized (mSetModeLock) {
synchronized (mDeviceStateLock) {
int btScoRequesterUid = bluetoothScoRequestOwnerUid();
@@ -2036,6 +2004,9 @@
}
}
}
+ if (msg.arg1 == 1 /*signal*/) {
+ mAudioService.decrementAudioModeResetCount();
+ }
break;
case MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT:
@@ -2109,40 +2080,9 @@
mDeviceInventory.setBluetoothActiveDevice(btInfo);
}
} break;
- case MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY: {
- final int strategy = msg.arg1;
- final List<AudioDeviceAttributes> devices =
- (List<AudioDeviceAttributes>) msg.obj;
- mDeviceInventory.onSaveSetPreferredDevices(strategy, devices);
- } break;
- case MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY: {
- final int strategy = msg.arg1;
- mDeviceInventory.onSaveRemovePreferredDevices(strategy);
- } break;
- case MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY: {
- final int strategy = msg.arg1;
- final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
- mDeviceInventory.onSaveSetDeviceAsNonDefault(strategy, device);
- } break;
- case MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY: {
- final int strategy = msg.arg1;
- final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj;
- mDeviceInventory.onSaveRemoveDeviceAsNonDefault(strategy, device);
- } break;
case MSG_CHECK_MUTE_MUSIC:
checkMessagesMuteMusic(0);
break;
- case MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET: {
- final int capturePreset = msg.arg1;
- final List<AudioDeviceAttributes> devices =
- (List<AudioDeviceAttributes>) msg.obj;
- mDeviceInventory.onSaveSetPreferredDevicesForCapturePreset(
- capturePreset, devices);
- } break;
- case MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET: {
- final int capturePreset = msg.arg1;
- mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset);
- } break;
case MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED: {
final BluetoothDevice btDevice = (BluetoothDevice) msg.obj;
BtHelper.onNotifyPreferredAudioProfileApplied(btDevice);
@@ -2224,7 +2164,7 @@
private static final int MSG_REPORT_NEW_ROUTES = 13;
private static final int MSG_II_SET_HEARING_AID_VOLUME = 14;
private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15;
- private static final int MSG_I_SET_MODE_OWNER = 16;
+ private static final int MSG_IL_SET_MODE_OWNER = 16;
private static final int MSG_I_BT_SERVICE_DISCONNECTED_PROFILE = 22;
private static final int MSG_IL_BT_SERVICE_CONNECTED_PROFILE = 23;
@@ -2235,16 +2175,10 @@
// process external command to (dis)connect a hearing aid device
private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 31;
- private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY = 32;
- private static final int MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY = 33;
-
private static final int MSG_L_COMMUNICATION_ROUTE_CLIENT_DIED = 34;
private static final int MSG_CHECK_MUTE_MUSIC = 35;
private static final int MSG_REPORT_NEW_ROUTES_A2DP = 36;
- private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET = 37;
- private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38;
-
private static final int MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT = 42;
private static final int MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43;
@@ -2253,8 +2187,6 @@
// process set volume for Le Audio, obj is BleVolumeInfo
private static final int MSG_II_SET_LE_AUDIO_OUT_VOLUME = 46;
- private static final int MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY = 47;
- private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48;
private static final int MSG_IIL_BTLEAUDIO_TIMEOUT = 49;
private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52;
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index a9bff8b..5fd12c2 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -548,14 +548,17 @@
@GuardedBy("mDevicesLock")
private final ArrayMap<Integer, String> mApmConnectedDevices = new ArrayMap<>();
+ @GuardedBy("mDevicesLock")
// List of preferred devices for strategies
private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevices =
new ArrayMap<>();
+ @GuardedBy("mDevicesLock")
// List of non-default devices for strategies
private final ArrayMap<Integer, List<AudioDeviceAttributes>> mNonDefaultDevices =
new ArrayMap<>();
+ @GuardedBy("mDevicesLock")
// List of preferred devices of capture preset
private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevicesForCapturePreset =
new ArrayMap<>();
@@ -808,24 +811,18 @@
synchronized (mDevicesLock) {
mAppliedStrategyRoles.clear();
mAppliedPresetRoles.clear();
- }
- synchronized (mPreferredDevices) {
mPreferredDevices.forEach((strategy, devices) -> {
setPreferredDevicesForStrategy(strategy, devices);
});
- }
- synchronized (mNonDefaultDevices) {
mNonDefaultDevices.forEach((strategy, devices) -> {
addDevicesRoleForStrategy(strategy, AudioSystem.DEVICE_ROLE_DISABLED,
devices, false /* internal */);
});
- }
- synchronized (mPreferredDevicesForCapturePreset) {
mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> {
setDevicesRoleForCapturePreset(
capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
});
- }
+ }
}
/** only public for mocking/spying, do not call outside of AudioService */
@@ -1225,7 +1222,8 @@
mmi.record();
}
- /*package*/ void onSaveSetPreferredDevices(int strategy,
+ @GuardedBy("mDevicesLock")
+ private void saveSetPreferredDevices(int strategy,
@NonNull List<AudioDeviceAttributes> devices) {
mPreferredDevices.put(strategy, devices);
List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
@@ -1243,12 +1241,14 @@
dispatchPreferredDevice(strategy, devices);
}
- /*package*/ void onSaveRemovePreferredDevices(int strategy) {
+ @GuardedBy("mDevicesLock")
+ private void saveRemovePreferredDevices(int strategy) {
mPreferredDevices.remove(strategy);
dispatchPreferredDevice(strategy, new ArrayList<AudioDeviceAttributes>());
}
- /*package*/ void onSaveSetDeviceAsNonDefault(int strategy,
+ @GuardedBy("mDevicesLock")
+ private void saveSetDeviceAsNonDefault(int strategy,
@NonNull AudioDeviceAttributes device) {
List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
if (nonDefaultDevices == null) {
@@ -1272,7 +1272,8 @@
}
}
- /*package*/ void onSaveRemoveDeviceAsNonDefault(int strategy,
+ @GuardedBy("mDevicesLock")
+ private void saveRemoveDeviceAsNonDefault(int strategy,
@NonNull AudioDeviceAttributes device) {
List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy);
if (nonDefaultDevices != null) {
@@ -1282,14 +1283,16 @@
}
}
- /*package*/ void onSaveSetPreferredDevicesForCapturePreset(
+ @GuardedBy("mDevicesLock")
+ private void saveSetPreferredDevicesForCapturePreset(
int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
mPreferredDevicesForCapturePreset.put(capturePreset, devices);
dispatchDevicesRoleForCapturePreset(
capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
}
- /*package*/ void onSaveClearPreferredDevicesForCapturePreset(int capturePreset) {
+ @GuardedBy("mDevicesLock")
+ private void saveClearPreferredDevicesForCapturePreset(int capturePreset) {
mPreferredDevicesForCapturePreset.remove(capturePreset);
dispatchDevicesRoleForCapturePreset(
capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED,
@@ -1301,21 +1304,22 @@
/*package*/ int setPreferredDevicesForStrategyAndSave(int strategy,
@NonNull List<AudioDeviceAttributes> devices) {
- final int status = setPreferredDevicesForStrategy(strategy, devices);
- if (status == AudioSystem.SUCCESS) {
- mDeviceBroker.postSaveSetPreferredDevicesForStrategy(strategy, devices);
+ synchronized(mDevicesLock){
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ final int status = setPreferredDevicesForStrategy(strategy, devices);
+ if (status == AudioSystem.SUCCESS) {
+ saveSetPreferredDevices(strategy, devices);
+ }
+ return status;
+ }
}
- return status;
}
// Only used for external requests coming from an API
/*package*/ int setPreferredDevicesForStrategy(int strategy,
@NonNull List<AudioDeviceAttributes> devices) {
- int status = AudioSystem.ERROR;
- try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
- status = setDevicesRoleForStrategy(
- strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices, false /* internal */);
- }
- return status;
+
+ return setDevicesRoleForStrategy(
+ strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices, false /* internal */);
}
// Only used for internal requests
/*package*/ int setPreferredDevicesForStrategyInt(int strategy,
@@ -1326,21 +1330,21 @@
}
/*package*/ int removePreferredDevicesForStrategyAndSave(int strategy) {
- final int status = removePreferredDevicesForStrategy(strategy);
- if (status == AudioSystem.SUCCESS) {
- mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy);
+ synchronized(mDevicesLock){
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ final int status = removePreferredDevicesForStrategy(strategy);
+ if (status == AudioSystem.SUCCESS) {
+ saveRemovePreferredDevices(strategy);
+ }
+ return status;
+ }
}
- return status;
}
// Only used for external requests coming from an API
/*package*/ int removePreferredDevicesForStrategy(int strategy) {
- int status = AudioSystem.ERROR;
- try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
- status = clearDevicesRoleForStrategy(
+ return clearDevicesRoleForStrategy(
strategy, AudioSystem.DEVICE_ROLE_PREFERRED, false /*internal */);
- }
- return status;
}
// Only used for internal requests
/*package*/ int removePreferredDevicesForStrategyInt(int strategy) {
@@ -1351,16 +1355,17 @@
/*package*/ int setDeviceAsNonDefaultForStrategyAndSave(int strategy,
@NonNull AudioDeviceAttributes device) {
int status = AudioSystem.ERROR;
-
- try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
- List<AudioDeviceAttributes> devices = new ArrayList<>();
- devices.add(device);
- status = addDevicesRoleForStrategy(
+ synchronized(mDevicesLock){
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ List<AudioDeviceAttributes> devices = new ArrayList<>();
+ devices.add(device);
+ status = addDevicesRoleForStrategy(
strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices, false /* internal */);
- }
- if (status == AudioSystem.SUCCESS) {
- mDeviceBroker.postSaveSetDeviceAsNonDefaultForStrategy(strategy, device);
+ if (status == AudioSystem.SUCCESS) {
+ saveSetDeviceAsNonDefault(strategy, device);
+ }
+ }
}
return status;
}
@@ -1368,16 +1373,17 @@
/*package*/ int removeDeviceAsNonDefaultForStrategyAndSave(int strategy,
@NonNull AudioDeviceAttributes device) {
int status = AudioSystem.ERROR;
-
- try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
- List<AudioDeviceAttributes> devices = new ArrayList<>();
- devices.add(device);
- status = removeDevicesRoleForStrategy(
+ synchronized(mDevicesLock){
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ List<AudioDeviceAttributes> devices = new ArrayList<>();
+ devices.add(device);
+ status = removeDevicesRoleForStrategy(
strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices, false /* internal */);
- }
- if (status == AudioSystem.SUCCESS) {
- mDeviceBroker.postSaveRemoveDeviceAsNonDefaultForStrategy(strategy, device);
+ if (status == AudioSystem.SUCCESS) {
+ saveRemoveDeviceAsNonDefault(strategy, device);
+ }
+ }
}
return status;
}
@@ -1405,41 +1411,40 @@
/*package*/ int setPreferredDevicesForCapturePresetAndSave(
int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
- final int status = setPreferredDevicesForCapturePreset(capturePreset, devices);
- if (status == AudioSystem.SUCCESS) {
- mDeviceBroker.postSaveSetPreferredDevicesForCapturePreset(capturePreset, devices);
+ synchronized(mDevicesLock){
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ final int status = setPreferredDevicesForCapturePreset(capturePreset, devices);
+ if (status == AudioSystem.SUCCESS) {
+ saveSetPreferredDevicesForCapturePreset(capturePreset, devices);
+ }
+ return status;
+ }
}
- return status;
}
// Only used for external requests coming from an API
private int setPreferredDevicesForCapturePreset(
int capturePreset, @NonNull List<AudioDeviceAttributes> devices) {
- int status = AudioSystem.ERROR;
- try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
- status = setDevicesRoleForCapturePreset(
+ return setDevicesRoleForCapturePreset(
capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices);
- }
- return status;
}
/*package*/ int clearPreferredDevicesForCapturePresetAndSave(int capturePreset) {
- final int status = clearPreferredDevicesForCapturePreset(capturePreset);
- if (status == AudioSystem.SUCCESS) {
- mDeviceBroker.postSaveClearPreferredDevicesForCapturePreset(capturePreset);
+ synchronized(mDevicesLock){
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ final int status = clearPreferredDevicesForCapturePreset(capturePreset);
+ if (status == AudioSystem.SUCCESS) {
+ saveClearPreferredDevicesForCapturePreset(capturePreset);
+ }
+ return status;
+ }
}
- return status;
}
// Only used for external requests coming from an API
private int clearPreferredDevicesForCapturePreset(int capturePreset) {
- int status = AudioSystem.ERROR;
-
- try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
- status = clearDevicesRoleForCapturePreset(
+ return clearDevicesRoleForCapturePreset(
capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED);
- }
- return status;
}
// Only used for internal requests
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index e83b036..561030e 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1915,7 +1915,7 @@
// Restore call state
synchronized (mDeviceBroker.mSetModeLock) {
onUpdateAudioMode(AudioSystem.MODE_CURRENT, android.os.Process.myPid(),
- mContext.getPackageName(), true /*force*/);
+ mContext.getPackageName(), true /*force*/, false /*signal*/);
}
final int forSys;
synchronized (mSettingsLock) {
@@ -4743,17 +4743,50 @@
}
}
if (updateAudioMode) {
- sendMsg(mAudioHandler,
- MSG_UPDATE_AUDIO_MODE,
- existingMsgPolicy,
- AudioSystem.MODE_CURRENT,
- android.os.Process.myPid(),
- mContext.getPackageName(),
- delay);
+ postUpdateAudioMode(existingMsgPolicy, AudioSystem.MODE_CURRENT,
+ android.os.Process.myPid(), mContext.getPackageName(),
+ false /*signal*/, delay);
}
}
}
+ static class UpdateAudioModeInfo {
+ UpdateAudioModeInfo(int mode, int pid, String packageName, boolean signal) {
+ mMode = mode;
+ mPid = pid;
+ mPackageName = packageName;
+ mSignal = signal;
+ }
+ private final int mMode;
+ private final int mPid;
+ private final String mPackageName;
+ private final boolean mSignal;
+
+ int getMode() {
+ return mMode;
+ }
+ int getPid() {
+ return mPid;
+ }
+ String getPackageName() {
+ return mPackageName;
+ }
+ boolean getSignal() {
+ return mSignal;
+ }
+ }
+
+ void postUpdateAudioMode(int msgPolicy, int mode, int pid, String packageName,
+ boolean signal, int delay) {
+ synchronized (mAudioModeResetLock) {
+ if (signal) {
+ mAudioModeResetCount++;
+ }
+ sendMsg(mAudioHandler, MSG_UPDATE_AUDIO_MODE, msgPolicy, 0, 0,
+ new UpdateAudioModeInfo(mode, pid, packageName, signal), delay);
+ }
+ }
+
private final IRecordingConfigDispatcher mVoiceRecordingActivityMonitor =
new IRecordingConfigDispatcher.Stub() {
@Override
@@ -6152,13 +6185,9 @@
} else {
SetModeDeathHandler h = mSetModeDeathHandlers.get(index);
mSetModeDeathHandlers.remove(index);
- sendMsg(mAudioHandler,
- MSG_UPDATE_AUDIO_MODE,
- SENDMSG_QUEUE,
- AudioSystem.MODE_CURRENT,
- android.os.Process.myPid(),
- mContext.getPackageName(),
- 0);
+ postUpdateAudioMode(SENDMSG_QUEUE, AudioSystem.MODE_CURRENT,
+ android.os.Process.myPid(), mContext.getPackageName(),
+ false /*signal*/, 0);
}
}
}
@@ -6404,19 +6433,14 @@
}
}
- sendMsg(mAudioHandler,
- MSG_UPDATE_AUDIO_MODE,
- SENDMSG_REPLACE,
- mode,
- pid,
- callingPackage,
- 0);
+ postUpdateAudioMode(SENDMSG_REPLACE, mode, pid, callingPackage,
+ hasModifyPhoneStatePermission && mode == AudioSystem.MODE_NORMAL, 0);
}
}
@GuardedBy("mDeviceBroker.mSetModeLock")
void onUpdateAudioMode(int requestedMode, int requesterPid, String requesterPackage,
- boolean force) {
+ boolean force, boolean signal) {
if (requestedMode == AudioSystem.MODE_CURRENT) {
requestedMode = getMode();
}
@@ -6431,7 +6455,7 @@
}
if (DEBUG_MODE) {
Log.v(TAG, "onUpdateAudioMode() new mode: " + mode + ", current mode: "
- + mMode.get() + " requested mode: " + requestedMode);
+ + mMode.get() + " requested mode: " + requestedMode + " signal: " + signal);
}
if (mode != mMode.get() || force) {
int status = AudioSystem.SUCCESS;
@@ -6477,7 +6501,7 @@
// when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
// connections not started by the application changing the mode when pid changes
- mDeviceBroker.postSetModeOwner(mode, pid, uid);
+ mDeviceBroker.postSetModeOwner(mode, pid, uid, signal);
} else {
Log.w(TAG, "onUpdateAudioMode: failed to set audio mode to: " + mode);
}
@@ -10162,7 +10186,7 @@
h.setRecordingActive(isRecordingActiveForUid(h.getUid()));
if (wasActive != h.isActive()) {
onUpdateAudioMode(AudioSystem.MODE_CURRENT, android.os.Process.myPid(),
- mContext.getPackageName(), false /*force*/);
+ mContext.getPackageName(), false /*force*/, false /*signal*/);
}
}
break;
@@ -10192,7 +10216,9 @@
case MSG_UPDATE_AUDIO_MODE:
synchronized (mDeviceBroker.mSetModeLock) {
- onUpdateAudioMode(msg.arg1, msg.arg2, (String) msg.obj, false /*force*/);
+ UpdateAudioModeInfo info = (UpdateAudioModeInfo) msg.obj;
+ onUpdateAudioMode(info.getMode(), info.getPid(), info.getPackageName(),
+ false /*force*/, info.getSignal());
}
break;
@@ -10895,9 +10921,59 @@
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
mmi.record();
+ //delay abandon focus requests from Telecom if an audio mode reset from Telecom
+ // is still being processed
+ final boolean abandonFromTelecom = (mContext.checkCallingOrSelfPermission(
+ MODIFY_PHONE_STATE) == PackageManager.PERMISSION_GRANTED)
+ && ((aa != null && aa.getUsage() == AudioAttributes.USAGE_VOICE_COMMUNICATION)
+ || AudioSystem.IN_VOICE_COMM_FOCUS_ID.equals(clientId));
+ if (abandonFromTelecom) {
+ synchronized (mAudioModeResetLock) {
+ final long start = java.lang.System.currentTimeMillis();
+ long elapsed = 0;
+ while (mAudioModeResetCount > 0) {
+ if (DEBUG_MODE) {
+ Log.i(TAG, "Abandon focus from Telecom, waiting for mode change");
+ }
+ try {
+ mAudioModeResetLock.wait(
+ AUDIO_MODE_RESET_TIMEOUT_MS - elapsed);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted while waiting for audio mode reset");
+ }
+ elapsed = java.lang.System.currentTimeMillis() - start;
+ if (elapsed >= AUDIO_MODE_RESET_TIMEOUT_MS) {
+ Log.e(TAG, "Timeout waiting for audio mode reset");
+ break;
+ }
+ }
+ if (DEBUG_MODE && elapsed != 0) {
+ Log.i(TAG, "Abandon focus from Telecom done waiting");
+ }
+ }
+ }
return mMediaFocusControl.abandonAudioFocus(fd, clientId, aa, callingPackageName);
}
+ /** synchronization between setMode(NORMAL) and abandonAudioFocus() frmo Telecom */
+ private static final long AUDIO_MODE_RESET_TIMEOUT_MS = 3000;
+
+ private final Object mAudioModeResetLock = new Object();
+
+ @GuardedBy("mAudioModeResetLock")
+ private int mAudioModeResetCount = 0;
+
+ void decrementAudioModeResetCount() {
+ synchronized (mAudioModeResetLock) {
+ if (mAudioModeResetCount > 0) {
+ mAudioModeResetCount--;
+ } else {
+ Log.w(TAG, "mAudioModeResetCount already 0");
+ }
+ mAudioModeResetLock.notify();
+ }
+ }
+
/** see {@link AudioManager#abandonAudioFocusForTest(AudioFocusRequest, String)} */
public int abandonAudioFocusForTest(IAudioFocusDispatcher fd, String clientId,
AudioAttributes aa, String callingPackageName) {
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index ce92dfb..b421264 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -605,7 +605,11 @@
break;
case BluetoothProfile.LE_AUDIO:
if (mLeAudio != null && mLeAudioCallback != null) {
- mLeAudio.unregisterCallback(mLeAudioCallback);
+ try {
+ mLeAudio.unregisterCallback(mLeAudioCallback);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while unregistering callback for LE audio", e);
+ }
}
mLeAudio = null;
mLeAudioCallback = null;
@@ -682,12 +686,21 @@
return;
}
if (mLeAudio != null && mLeAudioCallback != null) {
- mLeAudio.unregisterCallback(mLeAudioCallback);
+ try {
+ mLeAudio.unregisterCallback(mLeAudioCallback);
+ } catch (Exception e) {
+ Log.e(TAG, "Exception while unregistering callback for LE audio", e);
+ }
}
mLeAudio = (BluetoothLeAudio) proxy;
mLeAudioCallback = new MyLeAudioCallback();
- mLeAudio.registerCallback(
- mContext.getMainExecutor(), mLeAudioCallback);
+ try{
+ mLeAudio.registerCallback(
+ mContext.getMainExecutor(), mLeAudioCallback);
+ } catch (Exception e) {
+ mLeAudioCallback = null;
+ Log.e(TAG, "Exception while registering callback for LE audio", e);
+ }
break;
case BluetoothProfile.A2DP_SINK:
case BluetoothProfile.LE_AUDIO_BROADCAST:
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index dc263c5..af9c9ac 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -1080,7 +1080,7 @@
*/
public float[] getNits() {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mNits;
+ return mEvenDimmerBrightnessData.nits;
}
return mNits;
}
@@ -1093,7 +1093,7 @@
@VisibleForTesting
public float[] getBacklight() {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mBacklight;
+ return mEvenDimmerBrightnessData.backlight;
}
return mBacklight;
}
@@ -1107,7 +1107,7 @@
*/
public float getBacklightFromBrightness(float brightness) {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mBrightnessToBacklight.interpolate(brightness);
+ return mEvenDimmerBrightnessData.brightnessToBacklight.interpolate(brightness);
}
return mBrightnessToBacklightSpline.interpolate(brightness);
}
@@ -1120,7 +1120,7 @@
*/
public float getBrightnessFromBacklight(float backlight) {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mBacklightToBrightness.interpolate(backlight);
+ return mEvenDimmerBrightnessData.backlightToBrightness.interpolate(backlight);
}
return mBacklightToBrightnessSpline.interpolate(backlight);
}
@@ -1131,7 +1131,7 @@
*/
private Spline getBacklightToBrightnessSpline() {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mBacklightToBrightness;
+ return mEvenDimmerBrightnessData.backlightToBrightness;
}
return mBacklightToBrightnessSpline;
}
@@ -1144,11 +1144,11 @@
*/
public float getNitsFromBacklight(float backlight) {
if (mEvenDimmerBrightnessData != null) {
- if (mEvenDimmerBrightnessData.mBacklightToNits == null) {
+ if (mEvenDimmerBrightnessData.backlightToNits == null) {
return INVALID_NITS;
}
backlight = Math.max(backlight, mBacklightMinimum);
- return mEvenDimmerBrightnessData.mBacklightToNits.interpolate(backlight);
+ return mEvenDimmerBrightnessData.backlightToNits.interpolate(backlight);
}
if (mBacklightToNitsSpline == null) {
@@ -1165,14 +1165,14 @@
*/
public float getBacklightFromNits(float nits) {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mNitsToBacklight.interpolate(nits);
+ return mEvenDimmerBrightnessData.nitsToBacklight.interpolate(nits);
}
return mNitsToBacklightSpline.interpolate(nits);
}
private Spline getNitsToBacklightSpline() {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mNitsToBacklight;
+ return mEvenDimmerBrightnessData.nitsToBacklight;
}
return mNitsToBacklightSpline;
}
@@ -1186,7 +1186,7 @@
if (mEvenDimmerBrightnessData == null) {
return INVALID_NITS;
}
- return mEvenDimmerBrightnessData.mMinLuxToNits.interpolate(lux);
+ return mEvenDimmerBrightnessData.minLuxToNits.interpolate(lux);
}
/**
@@ -1197,7 +1197,7 @@
if (mEvenDimmerBrightnessData == null) {
return PowerManager.BRIGHTNESS_MIN;
}
- return mEvenDimmerBrightnessData.mTransitionPoint;
+ return mEvenDimmerBrightnessData.transitionPoint;
}
/**
@@ -1268,7 +1268,7 @@
*/
public float[] getBrightness() {
if (mEvenDimmerBrightnessData != null) {
- return mEvenDimmerBrightnessData.mBrightness;
+ return mEvenDimmerBrightnessData.brightness;
}
return mBrightness;
}
@@ -2617,13 +2617,13 @@
List<NonNegativeFloatToFloatPoint> points = map.getMap().getPoint();
for (NonNegativeFloatToFloatPoint point : points) {
float lux = point.getFirst().floatValue();
- float maxBrightness = point.getSecond().floatValue();
- if (maxBrightness > hbmTransitionPoint) {
+ float maxBacklight = point.getSecond().floatValue();
+ if (maxBacklight > hbmTransitionPoint) {
Slog.wtf(TAG,
- "Invalid NBM config: maxBrightness is greater than hbm"
+ "Invalid NBM config: maxBacklight is greater than hbm"
+ ".transitionPoint. type="
- + type + "; lux=" + lux + "; maxBrightness="
- + maxBrightness);
+ + type + "; lux=" + lux + "; maxBacklight="
+ + maxBacklight);
continue;
}
if (luxToTransitionPointMap.containsKey(lux)) {
@@ -2632,8 +2632,7 @@
+ lux);
continue;
}
- luxToTransitionPointMap.put(lux,
- getBrightnessFromBacklight(maxBrightness));
+ luxToTransitionPointMap.put(lux, getBrightnessFromBacklight(maxBacklight));
}
if (!luxToTransitionPointMap.isEmpty()) {
mLuxThrottlingData.put(mappedType, luxToTransitionPointMap);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index ae33b83..88907e3 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -5293,6 +5293,17 @@
}
@Override
+ public IntArray getDisplayIds() {
+ IntArray displayIds = new IntArray();
+ synchronized (mSyncRoot) {
+ mLogicalDisplayMapper.forEachLocked((logicalDisplay -> {
+ displayIds.add(logicalDisplay.getDisplayIdLocked());
+ }), /* includeDisabled= */ false);
+ }
+ return displayIds;
+ }
+
+ @Override
public DisplayManagerInternal.DisplayOffloadSession registerDisplayOffloader(
int displayId, @NonNull DisplayManagerInternal.DisplayOffloader displayOffloader) {
if (!mFlags.isDisplayOffloadEnabled()) {
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 711bc79..62d8761 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -22,6 +22,7 @@
import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
+import static com.android.server.display.brightness.BrightnessEvent.FLAG_EVEN_DIMMER;
import static com.android.server.display.config.DisplayBrightnessMappingConfig.autoBrightnessPresetToString;
import android.animation.Animator;
@@ -1755,6 +1756,8 @@
final float brightnessOnAvailableScale = MathUtils.constrainedMap(0.0f, 1.0f,
clampedState.getMinBrightness(), clampedMax,
brightnessState);
+ final boolean evenDimmerModeOn =
+ mCdsi != null && mCdsi.getReduceBrightColorsActivatedForEvenDimmer();
mTempBrightnessEvent.setPercent(Math.round(
1000.0f * com.android.internal.display.BrightnessUtils.convertLinearToGamma(
brightnessOnAvailableScale) / 10)); // rounded to one dp
@@ -1769,7 +1772,8 @@
mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode());
mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()
| (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)
- | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0));
+ | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0)
+ | (evenDimmerModeOn ? FLAG_EVEN_DIMMER : 0));
mTempBrightnessEvent.setRbcStrength(mCdsi != null
? mCdsi.getReduceBrightColorsStrength() : -1);
mTempBrightnessEvent.setPowerFactor(mPowerRequest.screenLowPowerBrightnessFactor);
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 0570b2a..5edea0a 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -37,9 +37,9 @@
import android.os.Trace;
import android.util.DisplayUtils;
import android.util.LongSparseArray;
-import android.util.MathUtils;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.Spline;
import android.view.Display;
import android.view.DisplayAddress;
import android.view.DisplayCutout;
@@ -81,10 +81,6 @@
private static final String UNIQUE_ID_PREFIX = "local:";
private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.boot.emulator.circular";
- // Min and max strengths for even dimmer feature.
- private static final float EVEN_DIMMER_MIN_STRENGTH = 0.0f;
- private static final float EVEN_DIMMER_MAX_STRENGTH = 90.0f;
- private static final float BRIGHTNESS_MIN = 0.0f;
private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>();
@@ -99,7 +95,9 @@
private Context mOverlayContext;
private int mEvenDimmerStrength = -1;
+ private boolean mEvenDimmerEnabled = false;
private ColorDisplayService.ColorDisplayServiceInternal mCdsi;
+ private Spline mNitsToEvenDimmerStrength;
// Called with SyncRoot lock held.
LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context,
@@ -279,7 +277,7 @@
mIsFirstDisplay = isFirstDisplay;
updateDisplayPropertiesLocked(staticDisplayInfo, dynamicInfo, modeSpecs);
mSidekickInternal = LocalServices.getService(SidekickInternal.class);
- mBacklightAdapter = new BacklightAdapter(displayToken, isFirstDisplay,
+ mBacklightAdapter = mInjector.getBacklightAdapter(displayToken, isFirstDisplay,
mSurfaceControlProxy);
mActiveSfDisplayModeAtStartId = dynamicInfo.activeDisplayModeId;
}
@@ -998,26 +996,36 @@
}
private void applyColorMatrixBasedDimming(float brightnessState) {
- int strength = (int) (MathUtils.constrainedMap(
- // to this range:
- EVEN_DIMMER_MAX_STRENGTH, EVEN_DIMMER_MIN_STRENGTH,
- // from this range:
- BRIGHTNESS_MIN, mDisplayDeviceConfig.getEvenDimmerTransitionPoint(),
- // map this (+ rounded up):
- brightnessState) + 0.5);
-
- if (mEvenDimmerStrength < 0 // uninitialised
- || MathUtils.abs(mEvenDimmerStrength - strength) > 1
- || strength <= 1) {
- mEvenDimmerStrength = strength;
- }
- boolean enabled = mEvenDimmerStrength > 0.0f;
-
if (mCdsi == null) {
mCdsi = LocalServices.getService(
ColorDisplayService.ColorDisplayServiceInternal.class);
}
- if (mCdsi != null) {
+ if (mCdsi == null) {
+ return;
+ }
+
+ final float minHardwareNits = backlightToNits(brightnessToBacklight(
+ mDisplayDeviceConfig.getEvenDimmerTransitionPoint()));
+ final float requestedNits =
+ backlightToNits(brightnessToBacklight(brightnessState));
+ mNitsToEvenDimmerStrength =
+ mCdsi.fetchEvenDimmerSpline(minHardwareNits);
+
+ if (mNitsToEvenDimmerStrength == null) {
+ return;
+ }
+
+ // Find required dimming strength, rounded up.
+ int strength = Math.round(mNitsToEvenDimmerStrength
+ .interpolate(requestedNits));
+ boolean enabled = strength > 0.0f;
+ if (mEvenDimmerEnabled != enabled) {
+ Slog.i(TAG, "Setting Extra Dim; strength: " + strength
+ + ", " + (enabled ? "enabled" : "disabled"));
+ }
+ if (mEvenDimmerStrength != strength || mEvenDimmerEnabled != enabled) {
+ mEvenDimmerEnabled = enabled;
+ mEvenDimmerStrength = strength;
mCdsi.applyEvenDimmerColorChanges(enabled, strength);
}
}
@@ -1290,6 +1298,9 @@
pw.println("DisplayDeviceConfig: ");
pw.println("---------------------");
pw.println(mDisplayDeviceConfig);
+ pw.println("mEvenDimmerEnabled=" + mEvenDimmerEnabled);
+ pw.println("mEvenDimmerStrength=" + mEvenDimmerStrength);
+ pw.println("mNitsToEvenDimmerStrength=" + mNitsToEvenDimmerStrength);
}
private int findSfDisplayModeIdLocked(int displayModeId, int modeGroup) {
@@ -1461,6 +1472,12 @@
long physicalDisplayId, boolean isFirstDisplay, DisplayManagerFlags flags) {
return DisplayDeviceConfig.create(context, physicalDisplayId, isFirstDisplay, flags);
}
+
+ public BacklightAdapter getBacklightAdapter(IBinder displayToken, boolean isFirstDisplay,
+ SurfaceControlProxy surfaceControlProxy) {
+ return new BacklightAdapter(displayToken, isFirstDisplay, surfaceControlProxy);
+
+ }
}
public interface DisplayEventListener {
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
index ad57ebf..9e9b899 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
@@ -44,6 +44,7 @@
public static final int FLAG_DOZE_SCALE = 0x4;
public static final int FLAG_USER_SET = 0x8;
public static final int FLAG_LOW_POWER_MODE = 0x20;
+ public static final int FLAG_EVEN_DIMMER = 0x40;
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
@@ -492,6 +493,7 @@
+ ((mFlags & FLAG_RBC) != 0 ? "rbc " : "")
+ ((mFlags & FLAG_INVALID_LUX) != 0 ? "invalid_lux " : "")
+ ((mFlags & FLAG_DOZE_SCALE) != 0 ? "doze_scale " : "")
- + ((mFlags & FLAG_LOW_POWER_MODE) != 0 ? "low_power_mode " : "");
+ + ((mFlags & FLAG_LOW_POWER_MODE) != 0 ? "low_power_mode " : "")
+ + ((mFlags & FLAG_EVEN_DIMMER) != 0 ? "even_dimmer " : "");
}
}
diff --git a/services/core/java/com/android/server/display/color/ColorDisplayService.java b/services/core/java/com/android/server/display/color/ColorDisplayService.java
index bd759f3..dc59e66 100644
--- a/services/core/java/com/android/server/display/color/ColorDisplayService.java
+++ b/services/core/java/com/android/server/display/color/ColorDisplayService.java
@@ -70,6 +70,7 @@
import android.util.MathUtils;
import android.util.Slog;
import android.util.SparseIntArray;
+import android.util.Spline;
import android.view.Display;
import android.view.SurfaceControl;
import android.view.accessibility.AccessibilityManager;
@@ -114,6 +115,8 @@
Matrix.setIdentityM(MATRIX_IDENTITY, 0);
}
+ private static final int EVEN_DIMMER_MAX_PERCENT_ALLOWED = 100;
+
private static final int MSG_USER_CHANGED = 0;
private static final int MSG_SET_UP = 1;
private static final int MSG_APPLY_NIGHT_DISPLAY_IMMEDIATE = 2;
@@ -193,6 +196,9 @@
private final boolean mVisibleBackgroundUsersEnabled;
private final UserManagerService mUserManager;
+ private Spline mEvenDimmerSpline;
+ private boolean mEvenDimmerActivated;
+
public ColorDisplayService(Context context) {
super(context);
mHandler = new TintHandler(DisplayThread.get().getLooper());
@@ -1625,6 +1631,16 @@
}
/**
+ * Gets the adjusted nits, given a strength and nits.
+ * @param strength of reduce bright colors
+ * @param nits target nits
+ * @return the actual nits that would be output, given input nits and rbc strength.
+ */
+ public float getAdjustedNitsForStrength(float nits, int strength) {
+ return mReduceBrightColorsTintController.getAdjustedNitsForStrength(nits, strength);
+ }
+
+ /**
* Sets the listener and returns whether reduce bright colors is currently enabled.
*/
public boolean setReduceBrightColorsListener(ReduceBrightColorsListener listener) {
@@ -1644,6 +1660,14 @@
}
/**
+ *
+ * @return whether reduce bright colors is on, due to even dimmer being activated
+ */
+ public boolean getReduceBrightColorsActivatedForEvenDimmer() {
+ return mEvenDimmerActivated;
+ }
+
+ /**
* Gets the computed brightness, in nits, when the reduce bright colors feature is applied
* at the current strength.
*
@@ -1667,10 +1691,42 @@
* Applies tint changes for even dimmer feature.
*/
public void applyEvenDimmerColorChanges(boolean enabled, int strength) {
+ mEvenDimmerActivated = enabled;
mReduceBrightColorsTintController.setActivated(enabled);
mReduceBrightColorsTintController.setMatrix(strength);
mHandler.sendEmptyMessage(MSG_APPLY_REDUCE_BRIGHT_COLORS);
}
+
+ /**
+ * Get spline to map between requested nits, and required even dimmer strength.
+ * @return nits to strength spline
+ */
+ public Spline fetchEvenDimmerSpline(float nits) {
+ if (mEvenDimmerSpline == null) {
+ mEvenDimmerSpline = createNitsToStrengthSpline(nits);
+ }
+ return mEvenDimmerSpline;
+ }
+
+ /**
+ * Creates a spline mapping requested nits values, for each resulting strength value
+ * (in percent) for even dimmer.
+ * Returns null if coefficients are not initialised.
+ * @return spline from nits to strength
+ */
+ private Spline createNitsToStrengthSpline(float nits) {
+ final float[] requestedNits = new float[EVEN_DIMMER_MAX_PERCENT_ALLOWED + 1];
+ final float[] resultingStrength = new float[EVEN_DIMMER_MAX_PERCENT_ALLOWED + 1];
+ for (int i = 0; i <= EVEN_DIMMER_MAX_PERCENT_ALLOWED; i++) {
+ resultingStrength[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] = i;
+ requestedNits[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] =
+ getAdjustedNitsForStrength(nits, i);
+ if (requestedNits[EVEN_DIMMER_MAX_PERCENT_ALLOWED - i] == 0) {
+ return null;
+ }
+ }
+ return new Spline.LinearSpline(requestedNits, resultingStrength);
+ }
}
/**
diff --git a/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java b/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java
index f529c4c..4f6fbd0 100644
--- a/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java
+++ b/services/core/java/com/android/server/display/color/ReduceBrightColorsTintController.java
@@ -123,6 +123,14 @@
return computeComponentValue(mStrength) * nits;
}
+ /**
+ * Returns the effective brightness (in nits), which has been adjusted to account for the effect
+ * of the bright color reduction.
+ */
+ public float getAdjustedNitsForStrength(float nits, int strength) {
+ return computeComponentValue(strength) * nits;
+ }
+
private float computeComponentValue(int strengthLevel) {
final float percentageStrength = strengthLevel / 100f;
final float squaredPercentageStrength = percentageStrength * percentageStrength;
diff --git a/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java b/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java
index 7e2e10a..9a590a2 100644
--- a/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java
+++ b/services/core/java/com/android/server/display/config/EvenDimmerBrightnessData.java
@@ -34,72 +34,76 @@
/**
* Brightness value at which even dimmer methods are used.
*/
- public final float mTransitionPoint;
+ public final float transitionPoint;
/**
* Nits array, maps to mBacklight
*/
- public final float[] mNits;
+ public final float[] nits;
/**
* Backlight array, maps to mBrightness and mNits
*/
- public final float[] mBacklight;
+ public final float[] backlight;
/**
* Brightness array, maps to mBacklight
*/
- public final float[] mBrightness;
+ public final float[] brightness;
+
/**
* Spline, mapping between backlight and nits
*/
- public final Spline mBacklightToNits;
+ public final Spline backlightToNits;
+
/**
* Spline, mapping between nits and backlight
*/
- public final Spline mNitsToBacklight;
+ public final Spline nitsToBacklight;
+
/**
* Spline, mapping between brightness and backlight
*/
- public final Spline mBrightnessToBacklight;
+ public final Spline brightnessToBacklight;
+
/**
* Spline, mapping between backlight and brightness
*/
- public final Spline mBacklightToBrightness;
+ public final Spline backlightToBrightness;
/**
* Spline, mapping the minimum nits for each lux condition.
*/
- public final Spline mMinLuxToNits;
+ public final Spline minLuxToNits;
@VisibleForTesting
public EvenDimmerBrightnessData(float transitionPoint, float[] nits,
float[] backlight, float[] brightness, Spline backlightToNits,
Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness,
Spline minLuxToNits) {
- mTransitionPoint = transitionPoint;
- mNits = nits;
- mBacklight = backlight;
- mBrightness = brightness;
- mBacklightToNits = backlightToNits;
- mNitsToBacklight = nitsToBacklight;
- mBrightnessToBacklight = brightnessToBacklight;
- mBacklightToBrightness = backlightToBrightness;
- mMinLuxToNits = minLuxToNits;
+ this.transitionPoint = transitionPoint;
+ this.nits = nits;
+ this.backlight = backlight;
+ this.brightness = brightness;
+ this.backlightToNits = backlightToNits;
+ this.nitsToBacklight = nitsToBacklight;
+ this.brightnessToBacklight = brightnessToBacklight;
+ this.backlightToBrightness = backlightToBrightness;
+ this.minLuxToNits = minLuxToNits;
}
@Override
public String toString() {
return "EvenDimmerBrightnessData {"
- + "mTransitionPoint: " + mTransitionPoint
- + ", mNits: " + Arrays.toString(mNits)
- + ", mBacklight: " + Arrays.toString(mBacklight)
- + ", mBrightness: " + Arrays.toString(mBrightness)
- + ", mBacklightToNits: " + mBacklightToNits
- + ", mNitsToBacklight: " + mNitsToBacklight
- + ", mBrightnessToBacklight: " + mBrightnessToBacklight
- + ", mBacklightToBrightness: " + mBacklightToBrightness
- + ", mMinLuxToNits: " + mMinLuxToNits
+ + "transitionPoint: " + transitionPoint
+ + ", nits: " + Arrays.toString(nits)
+ + ", backlight: " + Arrays.toString(backlight)
+ + ", brightness: " + Arrays.toString(brightness)
+ + ", backlightToNits: " + backlightToNits
+ + ", nitsToBacklight: " + nitsToBacklight
+ + ", brightnessToBacklight: " + brightnessToBacklight
+ + ", backlightToBrightness: " + backlightToBrightness
+ + ", minLuxToNits: " + minLuxToNits
+ "} ";
}
@@ -122,7 +126,6 @@
if (map == null) {
return null;
}
- String interpolation = map.getInterpolation();
List<BrightnessPoint> brightnessPoints = map.getBrightnessPoint();
if (brightnessPoints.isEmpty()) {
@@ -169,22 +172,11 @@
++i;
}
- // Explicitly choose linear interpolation.
- if ("linear".equals(interpolation)) {
- return new EvenDimmerBrightnessData(transitionPoint, nits, backlight, brightness,
- new Spline.LinearSpline(backlight, nits),
- new Spline.LinearSpline(nits, backlight),
- new Spline.LinearSpline(brightness, backlight),
- new Spline.LinearSpline(backlight, brightness),
- new Spline.LinearSpline(minLux, minNits)
- );
- }
-
return new EvenDimmerBrightnessData(transitionPoint, nits, backlight, brightness,
- Spline.createSpline(backlight, nits),
- Spline.createSpline(nits, backlight),
- Spline.createSpline(brightness, backlight),
- Spline.createSpline(backlight, brightness),
+ new Spline.LinearSpline(backlight, nits),
+ new Spline.LinearSpline(nits, backlight),
+ new Spline.LinearSpline(brightness, backlight),
+ new Spline.LinearSpline(backlight, brightness),
Spline.createSpline(minLux, minNits)
);
}
diff --git a/services/core/java/com/android/server/flags/pinner.aconfig b/services/core/java/com/android/server/flags/pinner.aconfig
index 2f817db..3453668 100644
--- a/services/core/java/com/android/server/flags/pinner.aconfig
+++ b/services/core/java/com/android/server/flags/pinner.aconfig
@@ -9,8 +9,11 @@
}
flag {
- name: "skip_home_art_pins"
- namespace: "system_performance"
- description: "Ablation study flag that controls if home app odex/vdex files should be pinned in memory."
- bug: "340935152"
-}
\ No newline at end of file
+ name: "pin_global_quota"
+ namespace: "system_performance"
+ description: "This flag controls whether pinner will use a global quota or not"
+ bug: "340935152"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 9281267..99f7f12 100644
--- a/services/core/java/com/android/server/input/InputManagerInternal.java
+++ b/services/core/java/com/android/server/input/InputManagerInternal.java
@@ -23,6 +23,7 @@
import android.hardware.display.DisplayViewport;
import android.hardware.input.KeyGestureEvent;
import android.os.IBinder;
+import android.util.SparseBooleanArray;
import android.view.InputChannel;
import android.view.inputmethod.InputMethodSubtype;
@@ -45,9 +46,11 @@
/**
* Called by the power manager to tell the input manager whether it should start
- * watching for wake events.
+ * watching for wake events on given displays.
+ *
+ * @param displayInteractivities Map of display ids to their current interactive state.
*/
- public abstract void setInteractive(boolean interactive);
+ public abstract void setDisplayInteractivities(SparseBooleanArray displayInteractivities);
/**
* Toggles Caps Lock state for input device with specific id.
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index fd7479e..f045576 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -25,6 +25,7 @@
import static com.android.hardware.input.Flags.touchpadVisualizer;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER;
import android.Manifest;
import android.annotation.EnforcePermission;
@@ -95,6 +96,7 @@
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
@@ -117,6 +119,7 @@
import android.view.VerifiedInputEvent;
import android.view.ViewConfiguration;
import android.view.WindowManager;
+import android.view.WindowManagerPolicyConstants;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
@@ -325,6 +328,9 @@
// Manages Sticky modifier state
private final StickyModifierStateController mStickyModifierStateController;
private final KeyGestureController mKeyGestureController;
+ /** Fallback actions by key code */
+ private final SparseArray<KeyCharacterMap.FallbackAction> mFallbackActions =
+ new SparseArray<>();
// Manages Keyboard microphone mute led
private final KeyboardLedController mKeyboardLedController;
@@ -611,6 +617,7 @@
mKeyRemapper.systemRunning();
mPointerIconCache.systemRunning();
mKeyboardGlyphManager.systemRunning();
+ mKeyGestureController.systemRunning();
initKeyGestures();
}
@@ -2469,6 +2476,14 @@
mFocusEventDebugView.reportKeyEvent(event);
}
}
+ if (useKeyGestureEventHandler() && mKeyGestureController.interceptKeyBeforeQueueing(event,
+ policyFlags)) {
+ // If key gesture gets triggered, we send the event to policy with KEY_GESTURE flag
+ // indicating, the event is used in triggering a key gesture. We can't block event
+ // like Power or volume keys since policy might still want to handle it to change
+ // certain states.
+ policyFlags |= WindowManagerPolicyConstants.FLAG_KEY_GESTURE_TRIGGERED;
+ }
return mWindowManagerCallbacks.interceptKeyBeforeQueueing(event, policyFlags);
}
@@ -2511,6 +2526,74 @@
null, null, null) == PERMISSION_GRANTED;
}
+ // Native callback.
+ @SuppressWarnings("unused")
+ private KeyEvent dispatchUnhandledKey(IBinder focus, KeyEvent event, int policyFlags) {
+ if (interceptUnhandledKey(event, focus)) {
+ return null;
+ }
+ // TODO(b/358569822): Move fallback logic to KeyGestureController
+ if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
+ return null;
+ }
+ final KeyCharacterMap kcm = event.getKeyCharacterMap();
+ final int keyCode = event.getKeyCode();
+ final int metaState = event.getMetaState();
+ final boolean initialDown = event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getRepeatCount() == 0;
+
+ // Check for fallback actions specified by the key character map.
+ final KeyCharacterMap.FallbackAction fallbackAction;
+ if (initialDown) {
+ fallbackAction = kcm.getFallbackAction(keyCode, metaState);
+ } else {
+ fallbackAction = mFallbackActions.get(keyCode);
+ }
+
+ if (fallbackAction == null) {
+ return null;
+ }
+ KeyEvent fallbackEvent = null;
+ final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK;
+ fallbackEvent = KeyEvent.obtain(
+ event.getDownTime(), event.getEventTime(),
+ event.getAction(), fallbackAction.keyCode,
+ event.getRepeatCount(), fallbackAction.metaState,
+ event.getDeviceId(), event.getScanCode(),
+ flags, event.getSource(), event.getDisplayId(), null);
+
+ if (!interceptFallback(focus, fallbackEvent, policyFlags)) {
+ fallbackEvent.recycle();
+ fallbackEvent = null;
+ }
+
+ if (initialDown) {
+ mFallbackActions.put(keyCode, fallbackAction);
+ } else if (event.getAction() == KeyEvent.ACTION_UP) {
+ mFallbackActions.remove(keyCode);
+ fallbackAction.recycle();
+ }
+ return fallbackEvent;
+ }
+
+ private boolean interceptFallback(IBinder focusedToken, KeyEvent fallbackEvent,
+ int policyFlags) {
+ int actions = interceptKeyBeforeQueueing(fallbackEvent, policyFlags);
+ if ((actions & ACTION_PASS_TO_USER) == 0) {
+ return false;
+ }
+ long delayMillis = interceptKeyBeforeDispatching(focusedToken, fallbackEvent, policyFlags);
+ return delayMillis == 0 && !interceptUnhandledKey(fallbackEvent, focusedToken);
+ }
+
+ private boolean interceptUnhandledKey(KeyEvent event, IBinder focus) {
+ if (useKeyGestureEventHandler() && mKeyGestureController.interceptUnhandledKey(event,
+ focus)) {
+ return true;
+ }
+ return mWindowManagerCallbacks.interceptUnhandledKey(event, focus);
+ }
+
private void initKeyGestures() {
if (!useKeyGestureEventHandler()) {
return;
@@ -2566,12 +2649,6 @@
// Native callback.
@SuppressWarnings("unused")
- private KeyEvent dispatchUnhandledKey(IBinder focus, KeyEvent event, int policyFlags) {
- return mWindowManagerCallbacks.dispatchUnhandledKey(focus, event, policyFlags);
- }
-
- // Native callback.
- @SuppressWarnings("unused")
private void onPointerDownOutsideFocus(IBinder touchedToken) {
mWindowManagerCallbacks.onPointerDownOutsideFocus(touchedToken);
}
@@ -2986,9 +3063,9 @@
long interceptKeyBeforeDispatching(IBinder token, KeyEvent event, int policyFlags);
/**
- * Dispatch unhandled key
+ * Intercept unhandled key
*/
- KeyEvent dispatchUnhandledKey(IBinder token, KeyEvent event, int policyFlags);
+ boolean interceptUnhandledKey(KeyEvent event, IBinder token);
int getPointerLayer();
@@ -3261,10 +3338,22 @@
}
@Override
- public void setInteractive(boolean interactive) {
- mNative.setInteractive(interactive);
- mBatteryController.onInteractiveChanged(interactive);
- mKeyboardBacklightController.onInteractiveChanged(interactive);
+ public void setDisplayInteractivities(SparseBooleanArray displayInteractivities) {
+ boolean globallyInteractive = false;
+ ArraySet<Integer> nonInteractiveDisplays = new ArraySet<>();
+ for (int i = 0; i < displayInteractivities.size(); i++) {
+ final int displayId = displayInteractivities.keyAt(i);
+ final boolean displayInteractive = displayInteractivities.get(displayId);
+ if (displayInteractive) {
+ globallyInteractive = true;
+ } else {
+ nonInteractiveDisplays.add(displayId);
+ }
+ }
+ mNative.setNonInteractiveDisplays(
+ nonInteractiveDisplays.stream().mapToInt(Integer::intValue).toArray());
+ mBatteryController.onInteractiveChanged(globallyInteractive);
+ mKeyboardBacklightController.onInteractiveChanged(globallyInteractive);
}
// TODO(b/358569822): Remove this method from InputManagerInternal after key gesture
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index 4538b49..b488db5 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -16,14 +16,23 @@
package com.android.server.input;
+import static android.content.pm.PackageManager.FEATURE_LEANBACK;
+import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE;
+
+import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
import android.annotation.BinderThread;
import android.annotation.MainThread;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
+import android.content.ContentResolver;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
+import android.database.ContentObserver;
import android.hardware.input.AidlKeyGestureEvent;
import android.hardware.input.IKeyGestureEventListener;
import android.hardware.input.IKeyGestureHandler;
@@ -35,18 +44,23 @@
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.provider.Settings;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.InputDevice;
+import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.policy.KeyCombinationManager;
import java.util.ArrayDeque;
import java.util.HashSet;
@@ -82,20 +96,36 @@
private static final int SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY = 1;
private static final int LAST_SEARCH_KEY_BEHAVIOR = SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY;
+ // must match: config_keyChordPowerVolumeUp in config.xml
+ static final int POWER_VOLUME_UP_BEHAVIOR_NOTHING = 0;
+ static final int POWER_VOLUME_UP_BEHAVIOR_MUTE = 1;
+ static final int POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS = 2;
+
private final Context mContext;
private final Handler mHandler;
private final int mSystemPid;
+ private final KeyCombinationManager mKeyCombinationManager;
+ private final SettingsObserver mSettingsObserver;
// Pending actions
private boolean mPendingMetaAction;
private boolean mPendingCapsLockToggle;
private boolean mPendingHideRecentSwitcher;
- // Key behaviors
+ // Platform behaviors
private boolean mEnableBugReportKeyboardShortcut;
+ private boolean mHasFeatureWatch;
+ private boolean mHasFeatureLeanback;
+
+ // Key behaviors
private int mSearchKeyBehavior;
private int mSettingsKeyBehavior;
+ // Settings behaviors
+ private int mRingerToggleChord = Settings.Secure.VOLUME_HUSH_OFF;
+ private int mPowerVolUpBehavior;
+
+
// List of currently registered key gesture event listeners keyed by process pid
@GuardedBy("mKeyGestureEventListenerRecords")
private final SparseArray<KeyGestureEventListenerRecord>
@@ -128,12 +158,19 @@
return Integer.compare(p1, p2);
}
});
+ mKeyCombinationManager = new KeyCombinationManager(mHandler);
+ mSettingsObserver = new SettingsObserver(mHandler);
initBehaviors();
+ initKeyCombinationRules();
}
private void initBehaviors() {
mEnableBugReportKeyboardShortcut = "1".equals(SystemProperties.get("ro.debuggable"));
+ PackageManager pm = mContext.getPackageManager();
+ mHasFeatureWatch = pm.hasSystemFeature(FEATURE_WATCH);
+ mHasFeatureLeanback = pm.hasSystemFeature(FEATURE_LEANBACK);
+
Resources res = mContext.getResources();
mSearchKeyBehavior = res.getInteger(R.integer.config_searchKeyBehavior);
if (mSearchKeyBehavior < SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH
@@ -145,6 +182,251 @@
|| mSettingsKeyBehavior > LAST_SETTINGS_KEY_BEHAVIOR) {
mSettingsKeyBehavior = SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY;
}
+
+ mHandler.post(this::initBehaviorsFromSettings);
+ }
+
+ private void initBehaviorsFromSettings() {
+ ContentResolver resolver = mContext.getContentResolver();
+ mRingerToggleChord = Settings.Secure.getIntForUser(resolver,
+ Settings.Secure.VOLUME_HUSH_GESTURE, Settings.Secure.VOLUME_HUSH_OFF,
+ UserHandle.USER_CURRENT);
+
+ mPowerVolUpBehavior = Settings.Global.getInt(resolver,
+ Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
+ mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_keyChordPowerVolumeUp));
+ }
+
+ private void initKeyCombinationRules() {
+ if (!useKeyGestureEventHandler() || !useKeyGestureEventHandlerMultiPressGestures()) {
+ return;
+ }
+ // TODO(b/358569822): Handle Power, Back key properly since key combination gesture is
+ // captured here and rest of the Power, Back key behaviors are handled in PWM
+ final boolean screenshotChordEnabled = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_enableScreenshotChord);
+
+ if (screenshotChordEnabled) {
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_DOWN,
+ KeyEvent.KEYCODE_POWER) {
+ @Override
+ public boolean preCondition() {
+ return isKeyGestureSupported(
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
+ }
+
+ @Override
+ public void execute() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+
+ @Override
+ public void cancel() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+ });
+
+ if (mHasFeatureWatch) {
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_POWER,
+ KeyEvent.KEYCODE_STEM_PRIMARY) {
+ @Override
+ public boolean preCondition() {
+ return isKeyGestureSupported(
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD);
+ }
+
+ @Override
+ public void execute() {
+ handleKeyGesture(new int[]{KeyEvent.KEYCODE_POWER,
+ KeyEvent.KEYCODE_STEM_PRIMARY},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+ @Override
+ public void cancel() {
+ handleKeyGesture(new int[]{KeyEvent.KEYCODE_POWER,
+ KeyEvent.KEYCODE_STEM_PRIMARY},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+ });
+ }
+ }
+
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_DOWN,
+ KeyEvent.KEYCODE_VOLUME_UP) {
+ @Override
+ public boolean preCondition() {
+ return isKeyGestureSupported(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD);
+ }
+
+ @Override
+ public void execute() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP},
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+
+ @Override
+ public void cancel() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP},
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+ });
+
+ // Volume up + power can either be the "ringer toggle chord" or as another way to
+ // launch GlobalActions. This behavior can change at runtime so we must check behavior
+ // inside the TwoKeysCombinationRule.
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_VOLUME_UP,
+ KeyEvent.KEYCODE_POWER) {
+ @Override
+ public boolean preCondition() {
+ if (!isKeyGestureSupported(getGestureType())) {
+ return false;
+ }
+ switch (mPowerVolUpBehavior) {
+ case POWER_VOLUME_UP_BEHAVIOR_MUTE:
+ return mRingerToggleChord != Settings.Secure.VOLUME_HUSH_OFF;
+ default:
+ return true;
+ }
+ }
+ @Override
+ public void execute() {
+ int gestureType = getGestureType();
+ if (gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) {
+ return;
+ }
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_POWER},
+ gestureType, KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+ @Override
+ public void cancel() {
+ int gestureType = getGestureType();
+ if (gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) {
+ return;
+ }
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_POWER},
+ gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+
+ @KeyGestureEvent.KeyGestureType
+ private int getGestureType() {
+ switch (mPowerVolUpBehavior) {
+ case POWER_VOLUME_UP_BEHAVIOR_MUTE -> {
+ return KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD;
+ }
+ case POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS -> {
+ return KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS;
+ }
+ default -> {
+ return KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED;
+ }
+ }
+ }
+ });
+
+ if (mHasFeatureLeanback) {
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_BACK,
+ KeyEvent.KEYCODE_DPAD_DOWN) {
+ @Override
+ public boolean preCondition() {
+ return isKeyGestureSupported(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD);
+ }
+
+ @Override
+ public void execute() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+
+ @Override
+ public void cancel() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+ @Override
+ public long getKeyInterceptDelayMs() {
+ // Use a timeout of 0 to prevent additional latency in processing of
+ // this key. This will potentially cause some unwanted UI actions if the
+ // user does end up triggering the key combination later, but in most
+ // cases, the user will simply hit a single key, and this will allow us
+ // to process it without first waiting to see if the combination is
+ // going to be triggered.
+ return 0;
+ }
+ });
+
+ mKeyCombinationManager.addRule(
+ new KeyCombinationManager.TwoKeysCombinationRule(KeyEvent.KEYCODE_BACK,
+ KeyEvent.KEYCODE_DPAD_CENTER) {
+ @Override
+ public boolean preCondition() {
+ return isKeyGestureSupported(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT);
+ }
+
+ @Override
+ public void execute() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+ KeyGestureEvent.ACTION_GESTURE_START, 0);
+ }
+ @Override
+ public void cancel() {
+ handleKeyGesture(
+ new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE,
+ KeyGestureEvent.FLAG_CANCELLED);
+ }
+ @Override
+ public long getKeyInterceptDelayMs() {
+ return 0;
+ }
+ });
+ }
+ }
+
+ public void systemRunning() {
+ mSettingsObserver.observe();
+ }
+
+ public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
+ final boolean interactive = (policyFlags & FLAG_INTERACTIVE) != 0;
+ if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
+ return mKeyCombinationManager.interceptKey(event, interactive);
+ }
+ return false;
}
public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
@@ -153,9 +435,22 @@
// KeyGestureHandler (PWM is one of the handlers)
final int keyCode = event.getKeyCode();
final int deviceId = event.getDeviceId();
+ final int flags = event.getFlags();
final long keyConsumed = -1;
final long keyNotConsumed = 0;
+ if (mKeyCombinationManager.isKeyConsumed(event)) {
+ return keyConsumed;
+ }
+
+ if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
+ final long now = SystemClock.uptimeMillis();
+ final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(keyCode);
+ if (now < interceptTimeout) {
+ return interceptTimeout - now;
+ }
+ }
+
Set<Integer> consumedKeys = mConsumedKeysForDevice.get(deviceId);
if (consumedKeys == null) {
consumedKeys = new HashSet<>();
@@ -577,10 +872,71 @@
return false;
}
+ boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
+ final int keyCode = event.getKeyCode();
+ final int repeatCount = event.getRepeatCount();
+ final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
+ final int metaState = event.getModifiers();
+ final int deviceId = event.getDeviceId();
+ final int displayId = event.getDisplayId();
+
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_SPACE:
+ if (down && repeatCount == 0) {
+ // Handle keyboard layout switching. (CTRL + SPACE)
+ if (KeyEvent.metaStateHasModifiers(metaState & ~KeyEvent.META_SHIFT_MASK,
+ KeyEvent.META_CTRL_ON)) {
+ return handleKeyGesture(deviceId, new int[]{keyCode},
+ KeyEvent.META_CTRL_ON | (event.isShiftPressed()
+ ? KeyEvent.META_SHIFT_ON : 0),
+ KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+ focusedToken, /* flags = */0);
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_Z:
+ if (down && KeyEvent.metaStateHasModifiers(metaState,
+ KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON)) {
+ // Intercept the Accessibility keychord (CTRL + ALT + Z) for keyboard users.
+ return handleKeyGesture(deviceId, new int[]{keyCode},
+ KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+ focusedToken, /* flags = */0);
+ }
+ break;
+ case KeyEvent.KEYCODE_SYSRQ:
+ if (down && repeatCount == 0) {
+ return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+ KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+ focusedToken, /* flags = */0);
+ }
+ break;
+ case KeyEvent.KEYCODE_ESCAPE:
+ if (down && KeyEvent.metaStateHasNoModifiers(metaState) && repeatCount == 0) {
+ return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0,
+ KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId,
+ focusedToken, /* flags = */0);
+ }
+ break;
+ }
+
+ return false;
+ }
+
+ private boolean handleKeyGesture(int[] keycodes,
+ @KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) {
+ return handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0,
+ gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags);
+ }
+
@VisibleForTesting
boolean handleKeyGesture(int deviceId, int[] keycodes, int modifierState,
@KeyGestureEvent.KeyGestureType int gestureType, int action, int displayId,
- IBinder focusedToken, int flags) {
+ @Nullable IBinder focusedToken, int flags) {
return handleKeyGesture(createKeyGestureEvent(deviceId, keycodes,
modifierState, gestureType, action, displayId, flags), focusedToken);
}
@@ -628,7 +984,7 @@
@MainThread
private void notifyKeyGestureEvent(AidlKeyGestureEvent event) {
InputDevice device = getInputDevice(event.deviceId);
- if (device == null || device.isVirtual()) {
+ if (device == null) {
return;
}
if (event.action == KeyGestureEvent.ACTION_GESTURE_COMPLETE) {
@@ -822,6 +1178,27 @@
}
}
+ private class SettingsObserver extends ContentObserver {
+ private SettingsObserver(Handler handler) {
+ super(handler);
+ }
+
+ private void observe() {
+ ContentResolver resolver = mContext.getContentResolver();
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.VOLUME_HUSH_GESTURE), false, this,
+ UserHandle.USER_ALL);
+ resolver.registerContentObserver(Settings.Global.getUriFor(
+ Settings.Global.KEY_CHORD_POWER_VOLUME_UP), false, this,
+ UserHandle.USER_ALL);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ initBehaviorsFromSettings();
+ }
+ }
+
@Nullable
private InputDevice getInputDevice(int deviceId) {
InputManager inputManager = mContext.getSystemService(InputManager.class);
@@ -851,6 +1228,8 @@
ipw.println("mPendingHideRecentSwitcher = " + mPendingHideRecentSwitcher);
ipw.println("mSearchKeyBehavior = " + mSearchKeyBehavior);
ipw.println("mSettingsKeyBehavior = " + mSettingsKeyBehavior);
+ ipw.println("mRingerToggleChord = " + mRingerToggleChord);
+ ipw.println("mPowerVolUpBehavior = " + mPowerVolUpBehavior);
ipw.print("mKeyGestureEventListenerRecords = {");
synchronized (mKeyGestureEventListenerRecords) {
int size = mKeyGestureEventListenerRecords.size();
@@ -881,5 +1260,6 @@
ipw.println(ev);
}
ipw.decreaseIndent();
+ mKeyCombinationManager.dump("", ipw);
}
}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index d17e256..4404d63 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -141,7 +141,7 @@
void setShowTouches(boolean enabled);
- void setInteractive(boolean interactive);
+ void setNonInteractiveDisplays(int[] displayIds);
void reloadCalibration();
@@ -409,7 +409,7 @@
public native void setShowTouches(boolean enabled);
@Override
- public native void setInteractive(boolean interactive);
+ public native void setNonInteractiveDisplays(int[] displayIds);
@Override
public native void reloadCalibration();
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index a1e5ebc..cf0c5b0 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -180,7 +180,8 @@
@Override
public boolean onTouchEvent(MotionEvent event) {
- if (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE) {
+ if (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
+ || event.getClassification() == MotionEvent.CLASSIFICATION_PINCH) {
return false;
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 2bb2b7b..f0fb33e 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -4799,7 +4799,8 @@
userData.mImeBindingState.mFocusedWindowEditorInfo,
info.focusedWindowName, userData.mImeBindingState.mFocusedWindowSoftInputMode,
reason, userData.mInFullscreenMode, info.requestWindowName,
- info.imeControlTargetName, info.imeLayerTargetName, info.imeSurfaceParentName));
+ info.imeControlTargetName, info.imeLayerTargetName, info.imeSurfaceParentName,
+ userId));
if (statsToken != null) {
mImeTrackerService.onImmsUpdate(statsToken, info.requestWindowName);
@@ -6132,8 +6133,7 @@
dumpAsStringNoCheckForUser(userData, fd, pw, args, isCritical);
}
- // TODO(b/365868861): Make StartInputHistory, SoftInputShowHideHistory and ImeTracker per
- // user.
+ // TODO(b/365868861): Make StartInputHistory and ImeTracker multi-user aware.
synchronized (ImfLock.class) {
p.println(" mStartInputHistory:");
mStartInputHistory.dump(pw, " ");
diff --git a/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
index 3023603..8445632 100644
--- a/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
+++ b/services/core/java/com/android/server/inputmethod/SoftInputShowHideHistory.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.os.SystemClock;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
@@ -62,6 +63,8 @@
final String mImeTargetNameFromWm;
@Nullable
final String mImeSurfaceParentName;
+ @UserIdInt
+ final int mImeUserId;
Entry(ClientState client, EditorInfo editorInfo,
String focusedWindowName,
@@ -69,7 +72,7 @@
@SoftInputShowHideReason int reason,
boolean inFullscreenMode, String requestWindowName,
@Nullable String imeControlTargetName, @Nullable String imeTargetName,
- @Nullable String imeSurfaceParentName) {
+ @Nullable String imeSurfaceParentName, @UserIdInt int imeUserId) {
mClientState = client;
mEditorInfo = editorInfo;
mFocusedWindowName = focusedWindowName;
@@ -82,6 +85,7 @@
mImeControlTargetName = imeControlTargetName;
mImeTargetNameFromWm = imeTargetName;
mImeSurfaceParentName = imeSurfaceParentName;
+ mImeUserId = imeUserId;
}
}
@@ -102,7 +106,8 @@
continue;
}
pw.print(prefix);
- pw.println("SoftInputShowHide #" + entry.mSequenceNumber + ":");
+ pw.println("SoftInputShowHide[" + entry.mImeUserId + "] #"
+ + entry.mSequenceNumber + ":");
pw.print(prefix);
pw.println(" time=" + formatter.format(Instant.ofEpochMilli(entry.mWallTime))
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index dc62b27..c3a714b 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -4116,23 +4116,38 @@
}
@Override
- @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
- public boolean canBePromoted(String pkg, int uid) {
+ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+ public boolean appCanBePromoted(String pkg, int uid) {
checkCallerIsSystemOrSystemUiOrShell();
- if (!android.app.Flags.uiRichOngoing()) {
+ if (!android.app.Flags.apiRichOngoing()) {
return false;
}
return mPreferencesHelper.canBePromoted(pkg, uid);
}
@Override
- @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
- public void setCanBePromoted(String pkg, int uid, boolean promote) {
+ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+ public boolean canBePromoted(String callingPkg) {
+ checkCallerIsSameApp(callingPkg);
+ if (!android.app.Flags.apiRichOngoing()) {
+ return false;
+ }
+ return mPreferencesHelper.canBePromoted(callingPkg, Binder.getCallingUid());
+ }
+
+
+ /**
+ * Any changes from SystemUI or Settings should be fromUser == true. Any changes the
+ * allowlist should be fromUser == false.
+ */
+ @Override
+ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+ public void setCanBePromoted(String pkg, int uid, boolean promote, boolean fromUser) {
checkCallerIsSystemOrSystemUiOrShell();
- if (!android.app.Flags.uiRichOngoing()) {
+ if (!android.app.Flags.apiRichOngoing()) {
return;
}
- boolean changed = mPreferencesHelper.setCanBePromoted(pkg, uid, promote);
+ boolean changed = mPreferencesHelper.setCanBePromoted(pkg, uid, promote, fromUser);
if (changed) {
// check for pending/posted notifs from this app and update the flag
synchronized (mNotificationLock) {
@@ -7776,7 +7791,7 @@
return false;
}
- if (android.app.Flags.uiRichOngoing()) {
+ if (android.app.Flags.apiRichOngoing()) {
// This would normally be done in fixNotification(), but we need the channel info so
// it's done a little late
if (mPreferencesHelper.canBePromoted(pkg, notificationUid)
@@ -10740,7 +10755,7 @@
}
@GuardedBy("mNotificationLock")
- @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
private @NonNull List<NotificationRecord> findAppNotificationByListLocked(
ArrayList<NotificationRecord> list, String pkg, int userId) {
List<NotificationRecord> records = new ArrayList<>();
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index fcc8d2f..85c3957 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -40,6 +40,7 @@
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED;
import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED;
+import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@@ -192,10 +193,13 @@
/**
* All user-lockable fields for a given application.
*/
- @IntDef({LockableAppFields.USER_LOCKED_IMPORTANCE})
+ @IntDef({LockableAppFields.USER_LOCKED_IMPORTANCE,
+ LockableAppFields.USER_LOCKED_BUBBLE,
+ LockableAppFields.USER_LOCKED_PROMOTABLE})
public @interface LockableAppFields {
int USER_LOCKED_IMPORTANCE = 0x00000001;
int USER_LOCKED_BUBBLE = 0x00000002;
+ int USER_LOCKED_PROMOTABLE = 0x00000004;
}
private final Object mLock = new Object();
@@ -850,21 +854,27 @@
}
}
- @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
public boolean canBePromoted(String packageName, int uid) {
synchronized (mLock) {
return getOrCreatePackagePreferencesLocked(packageName, uid).canHavePromotedNotifs;
}
}
- @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
- public boolean setCanBePromoted(String packageName, int uid, boolean promote) {
+ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
+ public boolean setCanBePromoted(String packageName, int uid, boolean promote,
+ boolean fromUser) {
boolean changed = false;
synchronized (mLock) {
PackagePreferences pkgPrefs = getOrCreatePackagePreferencesLocked(packageName, uid);
- if (pkgPrefs.canHavePromotedNotifs != promote) {
- pkgPrefs.canHavePromotedNotifs = promote;
- changed = true;
+ if (fromUser || ((pkgPrefs.lockedAppFields & USER_LOCKED_PROMOTABLE) == 0)) {
+ if (pkgPrefs.canHavePromotedNotifs != promote) {
+ pkgPrefs.canHavePromotedNotifs = promote;
+ if (fromUser) {
+ pkgPrefs.lockedAppFields |= USER_LOCKED_PROMOTABLE;
+ }
+ changed = true;
+ }
}
}
// no need to send a ranking update because we need to update the flag value on all pending
@@ -3065,7 +3075,7 @@
boolean migrateToPm = false;
long creationTime;
- @FlaggedApi(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @FlaggedApi(android.app.Flags.FLAG_API_RICH_ONGOING)
boolean canHavePromotedNotifs = false;
@UserIdInt int userId;
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index be3adc1..7043ceb 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -144,6 +144,13 @@
}
flag {
+ name: "notification_minimalism"
+ namespace: "systemui"
+ description: "Minimize the notifications to show on the lockscreen."
+ bug: "330387368"
+}
+
+flag {
name: "notification_force_group_singletons"
namespace: "systemui"
description: "This flag enables forced auto-grouping singleton groups"
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index f6d9dc2..03a34f2 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -87,6 +87,7 @@
import com.android.internal.os.BackgroundThread;
import com.android.server.LocalServices;
import com.android.server.SystemService;
+import com.android.server.SystemService.TargetUser;
import com.android.server.ondeviceintelligence.callbacks.ListenableDownloadCallback;
import java.io.FileDescriptor;
@@ -194,9 +195,13 @@
mIsServiceEnabled = isServiceEnabled();
}
+ }
- //connect to remote services(if available) during boot phase.
- if (phase == SystemService.PHASE_THIRD_PARTY_APPS_CAN_START) {
+ @Override
+ public void onUserUnlocked(@NonNull TargetUser user) {
+ Slog.d(TAG, "onUserUnlocked: " + user.getUserHandle());
+ //connect to remote services(if available) during boot.
+ if(user.getUserHandle().equals(UserHandle.SYSTEM)) {
try {
ensureRemoteInferenceServiceInitialized();
ensureRemoteIntelligenceServiceInitialized();
diff --git a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
index 8410cff..fe9a859 100644
--- a/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/pdb/PersistentDataBlockService.java
@@ -171,7 +171,6 @@
static final int MAX_DATA_BLOCK_SIZE = 1024 * 100;
public static final int DIGEST_SIZE_BYTES = 32;
- private static final String OEM_UNLOCK_PROP = "sys.oem_unlock_allowed";
private static final String FLASH_LOCK_PROP = "ro.boot.flash.locked";
private static final String FLASH_LOCK_LOCKED = "1";
private static final String FLASH_LOCK_UNLOCKED = "0";
@@ -275,7 +274,6 @@
enforceChecksumValidity();
if (mFrpEnforced) {
automaticallyDeactivateFrpIfPossible();
- setOemUnlockEnabledProperty(doGetOemUnlockEnabled());
setOldSettingForBackworkCompatibility(mFrpActive);
} else {
formatIfOemUnlockEnabled();
@@ -303,10 +301,6 @@
}
}
- private void setOemUnlockEnabledProperty(boolean oemUnlockEnabled) {
- setProperty(OEM_UNLOCK_PROP, oemUnlockEnabled ? "1" : "0");
- }
-
@Override
public void onBootPhase(int phase) {
// Wait for initialization in onStart to finish
@@ -342,7 +336,6 @@
formatPartitionLocked(true);
}
}
- setOemUnlockEnabledProperty(enabled);
}
private void enforceOemUnlockReadPermission() {
@@ -814,17 +807,9 @@
channel.force(true);
} catch (IOException e) {
Slog.e(TAG, "unable to access persistent partition", e);
- return;
- } finally {
- setOemUnlockEnabledProperty(enabled);
}
}
- @VisibleForTesting
- void setProperty(String name, String value) {
- SystemProperties.set(name, value);
- }
-
private boolean doGetOemUnlockEnabled() {
DataInputStream inputStream;
try {
diff --git a/services/core/java/com/android/server/pinner/PinRangeSource.java b/services/core/java/com/android/server/pinner/PinRangeSource.java
new file mode 100644
index 0000000..5f96411
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSource.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pinner;
+
+/* package */ abstract class PinRangeSource {
+ /**
+ * Retrieve a range to pin.
+ *
+ * @param outPinRange Receives the pin region
+ * @return True if we filled in outPinRange or false if we're out of pin entries
+ */
+ abstract boolean read(PinnerService.PinRange outPinRange);
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java
new file mode 100644
index 0000000..d6fc487
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSourceStatic.java
@@ -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.server.pinner;
+
+/* package */ class PinRangeSourceStatic extends PinRangeSource {
+ private final int mPinStart;
+ private final int mPinLength;
+ private boolean mDone = false;
+
+ PinRangeSourceStatic(int pinStart, int pinLength) {
+ mPinStart = pinStart;
+ mPinLength = pinLength;
+ }
+
+ @Override
+ boolean read(PinnerService.PinRange outPinRange) {
+ outPinRange.start = mPinStart;
+ outPinRange.length = mPinLength;
+ boolean done = mDone;
+ mDone = true;
+ return !done;
+ }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinRangeSourceStream.java b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java
new file mode 100644
index 0000000..79900b9
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinRangeSourceStream.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pinner;
+
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/* package */ final class PinRangeSourceStream extends PinRangeSource {
+ private final DataInputStream mStream;
+ private boolean mDone = false;
+
+ PinRangeSourceStream(InputStream stream) {
+ mStream = new DataInputStream(stream);
+ }
+
+ @Override
+ boolean read(PinnerService.PinRange outPinRange) {
+ if (!mDone) {
+ try {
+ outPinRange.start = mStream.readInt();
+ outPinRange.length = mStream.readInt();
+ } catch (IOException ex) {
+ mDone = true;
+ }
+ }
+ return !mDone;
+ }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/pinner/PinnedFile.java b/services/core/java/com/android/server/pinner/PinnedFile.java
new file mode 100644
index 0000000..a8de344
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinnedFile.java
@@ -0,0 +1,61 @@
+/*
+ * 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.pinner;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+
+@VisibleForTesting
+public final class PinnedFile implements AutoCloseable {
+ private long mAddress;
+ final long mapSize;
+ final String fileName;
+ public final long bytesPinned;
+
+ // Whether this file was pinned using a pinlist
+ boolean used_pinlist;
+
+ // User defined group name for pinner accounting
+ String groupName = "";
+ ArrayList<PinnedFile> pinnedDeps = new ArrayList<>();
+
+ public PinnedFile(long address, long mapSize, String fileName, long bytesPinned) {
+ mAddress = address;
+ this.mapSize = mapSize;
+ this.fileName = fileName;
+ this.bytesPinned = bytesPinned;
+ }
+
+ @Override
+ public void close() {
+ if (mAddress >= 0) {
+ PinnerUtils.safeMunmap(mAddress, mapSize);
+ mAddress = -1;
+ }
+ for (PinnedFile dep : pinnedDeps) {
+ if (dep != null) {
+ dep.close();
+ }
+ }
+ }
+
+ @Override
+ public void finalize() {
+ close();
+ }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/pinner/PinnerService.java
similarity index 76%
rename from services/core/java/com/android/server/PinnerService.java
rename to services/core/java/com/android/server/pinner/PinnerService.java
index ef03888..d7ac520 100644
--- a/services/core/java/com/android/server/PinnerService.java
+++ b/services/core/java/com/android/server/pinner/PinnerService.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,14 +14,14 @@
* limitations under the License.
*/
-package com.android.server;
+package com.android.server.pinner;
import static android.app.ActivityManager.UID_OBSERVER_ACTIVE;
import static android.app.ActivityManager.UID_OBSERVER_GONE;
import static android.os.Process.SYSTEM_UID;
+import static com.android.server.flags.Flags.pinGlobalQuota;
import static com.android.server.flags.Flags.pinWebview;
-import static com.android.server.flags.Flags.skipHomeArtPins;
import android.annotation.EnforcePermission;
import android.annotation.IntDef;
@@ -49,6 +49,7 @@
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
@@ -72,13 +73,13 @@
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
import com.android.server.wm.ActivityTaskManagerInternal;
import dalvik.system.DexFile;
import dalvik.system.VMRuntime;
-import java.io.Closeable;
-import java.io.DataInputStream;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -110,8 +111,7 @@
private static final String PIN_META_FILENAME = "pinlist.meta";
private static final int PAGE_SIZE = (int) Os.sysconf(OsConstants._SC_PAGESIZE);
private static final int MATCH_FLAGS = PackageManager.MATCH_DEFAULT_ONLY
- | PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
private static final int KEY_CAMERA = 0;
private static final int KEY_HOME = 1;
@@ -126,6 +126,8 @@
public static final String ANON_REGION_STAT_NAME = "[anon]";
+ private static final String SYSTEM_GROUP_NAME = "system";
+
@IntDef({KEY_CAMERA, KEY_HOME, KEY_ASSISTANT})
@Retention(RetentionPolicy.SOURCE)
public @interface AppKey {}
@@ -139,7 +141,8 @@
private final UserManager mUserManager;
/** The list of the statically pinned files. */
- @GuardedBy("this") private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>();
+ @GuardedBy("this")
+ private final ArrayMap<String, PinnedFile> mPinnedFiles = new ArrayMap<>();
/** The list of the pinned apps. This is a map from {@link AppKey} to a pinned app. */
@GuardedBy("this")
@@ -159,8 +162,8 @@
/**
* A set of {@link AppKey} that are configured to be pinned.
*/
- @GuardedBy("this")
- private ArraySet<Integer> mPinKeys;
+ @GuardedBy("this") private
+ ArraySet<Integer> mPinKeys;
// Note that we don't use the `_BOOT` namespace for anonymous pinnings, as we want
// them to be responsive to dynamic flag changes for experimentation.
@@ -180,14 +183,23 @@
private final boolean mConfiguredToPinAssistant;
private final int mConfiguredWebviewPinBytes;
+ // This is the percentage of total device memory that will be used to set the global quota.
+ private final int mConfiguredMaxPinnedMemoryPercentage;
+
+ // This is the global pinner quota that can be pinned.
+ private long mConfiguredMaxPinnedMemory;
+
+ // This is the currently pinned memory.
+ private long mCurrentPinnedMemory = 0;
+
private BinderService mBinderService;
private PinnerHandler mPinnerHandler = null;
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
- // If an app has updated, update pinned files accordingly.
- if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
+ // If an app has updated, update pinned files accordingly.
+ if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
Uri packageUri = intent.getData();
String packageName = packageUri.getSchemeSpecificPart();
ArraySet<String> updatedPackages = new ArraySet<>();
@@ -210,7 +222,7 @@
/** Utility class for testing. */
@VisibleForTesting
- static class Injector {
+ public static class Injector {
protected DeviceConfigInterface getDeviceConfigInterface() {
return DeviceConfigInterface.REAL;
}
@@ -219,9 +231,9 @@
service.publishBinderService("pinner", binderService);
}
- protected PinnedFile pinFileInternal(String fileToPin,
- int maxBytesToPin, boolean attemptPinIntrospection) {
- return PinnerService.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection);
+ protected PinnedFile pinFileInternal(PinnerService service, String fileToPin,
+ long maxBytesToPin, boolean attemptPinIntrospection) {
+ return service.pinFileInternal(fileToPin, maxBytesToPin, attemptPinIntrospection);
}
}
@@ -230,7 +242,7 @@
}
@VisibleForTesting
- PinnerService(Context context, Injector injector) {
+ public PinnerService(Context context, Injector injector) {
super(context);
mContext = context;
@@ -244,6 +256,9 @@
com.android.internal.R.bool.config_pinnerAssistantApp);
mConfiguredWebviewPinBytes = context.getResources().getInteger(
com.android.internal.R.integer.config_pinnerWebviewPinBytes);
+ mConfiguredMaxPinnedMemoryPercentage = context.getResources().getInteger(
+ com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage);
+
mPinKeys = createPinKeys();
mPinnerHandler = new PinnerHandler(BackgroundThread.get().getLooper());
@@ -261,10 +276,8 @@
registerUidListener();
registerUserSetupCompleteListener();
- mDeviceConfigInterface.addOnPropertiesChangedListener(
- DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
- new HandlerExecutor(mPinnerHandler),
- mDeviceConfigAnonSizeListener);
+ mDeviceConfigInterface.addOnPropertiesChangedListener(DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
+ new HandlerExecutor(mPinnerHandler), mDeviceConfigAnonSizeListener);
}
@Override
@@ -272,6 +285,10 @@
if (DEBUG) {
Slog.i(TAG, "Starting PinnerService");
}
+ mConfiguredMaxPinnedMemory =
+ (Process.getTotalMemory()
+ * Math.clamp(mConfiguredMaxPinnedMemoryPercentage, 0, 100))
+ / 100;
mBinderService = new BinderService();
mInjector.publishBinderService(this, mBinderService);
publishLocalService(PinnerService.class, this);
@@ -348,7 +365,7 @@
protected PinnedFileStats(int uid, PinnedFile file) {
this.uid = uid;
this.filename = file.fileName.substring(file.fileName.lastIndexOf('/') + 1);
- this.sizeKb = file.bytesPinned / 1024;
+ this.sizeKb = (int) file.bytesPinned / 1024;
}
}
@@ -358,20 +375,11 @@
private void handlePinOnStart() {
// Files to pin come from the overlay and can be specified per-device config
String[] filesToPin = mContext.getResources().getStringArray(
- com.android.internal.R.array.config_defaultPinnerServiceFiles);
+ com.android.internal.R.array.config_defaultPinnerServiceFiles);
// Continue trying to pin each file even if we fail to pin some of them
for (String fileToPin : filesToPin) {
- PinnedFile pf = mInjector.pinFileInternal(fileToPin, Integer.MAX_VALUE,
- /*attemptPinIntrospection=*/false);
- if (pf == null) {
- Slog.e(TAG, "Failed to pin file = " + fileToPin);
- continue;
- }
- synchronized (this) {
- mPinnedFiles.put(pf.fileName, pf);
- }
- pf.groupName = "system";
- pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, null);
+ pinFile(fileToPin, Integer.MAX_VALUE, /*appInfo=*/null, /*groupName=*/SYSTEM_GROUP_NAME,
+ true);
}
refreshPinAnonConfig();
@@ -383,10 +391,9 @@
* regular home app.
*/
private void registerUserSetupCompleteListener() {
- Uri userSetupCompleteUri = Settings.Secure.getUriFor(
- Settings.Secure.USER_SETUP_COMPLETE);
- mContext.getContentResolver().registerContentObserver(userSetupCompleteUri,
- false, new ContentObserver(null) {
+ Uri userSetupCompleteUri = Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE);
+ mContext.getContentResolver().registerContentObserver(
+ userSetupCompleteUri, false, new ContentObserver(null) {
@Override
public void onChange(boolean selfChange, Uri uri) {
if (userSetupCompleteUri.equals(uri)) {
@@ -409,7 +416,7 @@
}
@Override
- public void onUidActive(int uid) {
+ public void onUidActive(int uid) {
mPinnerHandler.sendMessage(PooledLambda.obtainMessage(
PinnerService::handleUidActive, PinnerService.this, uid));
}
@@ -423,7 +430,6 @@
updateActiveState(uid, false /* active */);
int key;
synchronized (this) {
-
// In case we have a pending repin, repin now. See mPendingRepin for more information.
key = mPendingRepin.getOrDefault(uid, -1);
if (key == -1) {
@@ -491,8 +497,8 @@
private ApplicationInfo getCameraInfo(int userHandle) {
Intent cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
- ApplicationInfo info = getApplicationInfoForIntent(cameraIntent, userHandle,
- false /* defaultToSystemApp */);
+ ApplicationInfo info = getApplicationInfoForIntent(
+ cameraIntent, userHandle, false /* defaultToSystemApp */);
// If the STILL_IMAGE_CAMERA intent doesn't resolve, try the _SECURE intent.
// We don't use _SECURE first because it will never get set on a device
@@ -501,16 +507,16 @@
// preference using this intent.
if (info == null) {
cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE);
- info = getApplicationInfoForIntent(cameraIntent, userHandle,
- false /* defaultToSystemApp */);
+ info = getApplicationInfoForIntent(
+ cameraIntent, userHandle, false /* defaultToSystemApp */);
}
// If the _SECURE intent doesn't resolve, try the original intent but request
// the system app for camera if there was more than one result.
if (info == null) {
cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
- info = getApplicationInfoForIntent(cameraIntent, userHandle,
- true /* defaultToSystemApp */);
+ info = getApplicationInfoForIntent(
+ cameraIntent, userHandle, true /* defaultToSystemApp */);
}
return info;
}
@@ -525,14 +531,14 @@
return getApplicationInfoForIntent(intent, userHandle, true);
}
- private ApplicationInfo getApplicationInfoForIntent(Intent intent, int userHandle,
- boolean defaultToSystemApp) {
+ private ApplicationInfo getApplicationInfoForIntent(
+ Intent intent, int userHandle, boolean defaultToSystemApp) {
if (intent == null) {
return null;
}
- ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivityAsUser(intent,
- MATCH_FLAGS, userHandle);
+ ResolveInfo resolveInfo =
+ mContext.getPackageManager().resolveActivityAsUser(intent, MATCH_FLAGS, userHandle);
// If this intent can resolve to only one app, choose that one.
// Otherwise, if we've requested to default to the system app, return it;
@@ -547,12 +553,11 @@
}
if (defaultToSystemApp) {
- List<ResolveInfo> infoList = mContext.getPackageManager()
- .queryIntentActivitiesAsUser(intent, MATCH_FLAGS, userHandle);
+ List<ResolveInfo> infoList = mContext.getPackageManager().queryIntentActivitiesAsUser(
+ intent, MATCH_FLAGS, userHandle);
ApplicationInfo systemAppInfo = null;
for (ResolveInfo info : infoList) {
- if ((info.activityInfo.applicationInfo.flags
- & ApplicationInfo.FLAG_SYSTEM) != 0) {
+ if ((info.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
if (systemAppInfo == null) {
systemAppInfo = info.activityInfo.applicationInfo;
} else {
@@ -568,13 +573,13 @@
}
private void sendPinAppsMessage(int userHandle) {
- mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApps, this,
- userHandle));
+ mPinnerHandler.sendMessage(
+ PooledLambda.obtainMessage(PinnerService::pinApps, this, userHandle));
}
private void sendPinAppsWithUpdatedKeysMessage(int userHandle) {
- mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinAppsWithUpdatedKeys,
- this, userHandle));
+ mPinnerHandler.sendMessage(PooledLambda.obtainMessage(
+ PinnerService::pinAppsWithUpdatedKeys, this, userHandle));
}
private void sendUnpinAppsMessage() {
mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::unpinApps, this));
@@ -586,8 +591,7 @@
// phenotype property is not set.
boolean shouldPinCamera = mConfiguredToPinCamera
&& mDeviceConfigInterface.getBoolean(DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT,
- "pin_camera",
- SystemProperties.getBoolean("pinner.pin_camera", true));
+ "pin_camera", SystemProperties.getBoolean("pinner.pin_camera", true));
if (shouldPinCamera) {
pinKeys.add(KEY_CAMERA);
} else if (DEBUG) {
@@ -626,8 +630,9 @@
synchronized (this) {
// This code path demands preceding unpinApps() call.
if (!mPinnedApps.isEmpty()) {
- Slog.e(TAG, "Attempted to update a list of apps, "
- + "but apps were already pinned. Skipping.");
+ Slog.e(TAG,
+ "Attempted to update a list of apps, "
+ + "but apps were already pinned. Skipping.");
return;
}
@@ -646,8 +651,8 @@
* @see #pinApp(int, int, boolean)
*/
private void sendPinAppMessage(int key, int userHandle, boolean force) {
- mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApp, this,
- key, userHandle, force));
+ mPinnerHandler.sendMessage(
+ PooledLambda.obtainMessage(PinnerService::pinApp, this, key, userHandle, force));
}
/**
@@ -667,10 +672,10 @@
}
return;
}
- unpinApp(key);
ApplicationInfo info = getInfoForKey(key, userHandle);
+ unpinApp(key);
if (info != null) {
- pinApp(key, info);
+ pinAppInternal(key, info);
}
}
@@ -682,9 +687,7 @@
private int getUidForKey(@AppKey int key) {
synchronized (this) {
PinnedApp existing = mPinnedApps.get(key);
- return existing != null && existing.active
- ? existing.uid
- : -1;
+ return existing != null && existing.active ? existing.uid : -1;
}
}
@@ -727,11 +730,8 @@
* Handle any changes in the anon region pinner config.
*/
private void refreshPinAnonConfig() {
- long newPinAnonSize =
- mDeviceConfigInterface.getLong(
- DEVICE_CONFIG_NAMESPACE_ANON_SIZE,
- DEVICE_CONFIG_KEY_ANON_SIZE,
- DEFAULT_ANON_SIZE);
+ long newPinAnonSize = mDeviceConfigInterface.getLong(
+ DEVICE_CONFIG_NAMESPACE_ANON_SIZE, DEVICE_CONFIG_KEY_ANON_SIZE, DEFAULT_ANON_SIZE);
newPinAnonSize = Math.max(0, Math.min(newPinAnonSize, MAX_ANON_SIZE));
if (newPinAnonSize != mPinAnonSize) {
mPinAnonSize = newPinAnonSize;
@@ -765,10 +765,9 @@
try {
// Map as SHARED to avoid changing rss.anon for system_server (per /proc/*/status).
// The mapping is visible in other rss metrics, and as private dirty in smaps/meminfo.
- address = Os.mmap(0, alignedPinSize,
- OsConstants.PROT_READ | OsConstants.PROT_WRITE,
- OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS,
- new FileDescriptor(), /*offset=*/0);
+ address = Os.mmap(0, alignedPinSize, OsConstants.PROT_READ | OsConstants.PROT_WRITE,
+ OsConstants.MAP_SHARED | OsConstants.MAP_ANONYMOUS, new FileDescriptor(),
+ /*offset=*/0);
Unsafe tempUnsafe = null;
Class<sun.misc.Unsafe> clazz = sun.misc.Unsafe.class;
@@ -794,14 +793,14 @@
return;
} finally {
if (address >= 0) {
- safeMunmap(address, alignedPinSize);
+ PinnerUtils.safeMunmap(address, alignedPinSize);
}
}
}
private void unpinAnonRegion() {
if (mPinAnonAddress != 0) {
- safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize);
+ PinnerUtils.safeMunmap(mPinAnonAddress, mCurrentlyPinnedAnonSize);
}
mPinAnonAddress = 0;
mCurrentlyPinnedAnonSize = 0;
@@ -824,12 +823,20 @@
}
/**
+ * Retrieves remaining quota for pinner service, once it reaches 0 it will no longer
+ * pin any file.
+ */
+ private long getAvailableGlobalQuota() {
+ return mConfiguredMaxPinnedMemory - mCurrentPinnedMemory;
+ }
+
+ /**
* Pins an application.
*
* @param key The key of the app to pin.
* @param appInfo The corresponding app info.
*/
- private void pinApp(@AppKey int key, @Nullable ApplicationInfo appInfo) {
+ private void pinAppInternal(@AppKey int key, @Nullable ApplicationInfo appInfo) {
if (appInfo == null) {
return;
}
@@ -839,7 +846,6 @@
mPinnedApps.put(key, pinnedApp);
}
-
// pin APK
final int pinSizeLimit = getSizeLimitForKey(key);
List<String> apks = new ArrayList<>();
@@ -851,36 +857,31 @@
}
}
- int apkPinSizeLimit = pinSizeLimit;
+ long apkPinSizeLimit = pinSizeLimit;
- boolean shouldSkipArtPins = key == KEY_HOME && skipHomeArtPins();
-
- for (String apk: apks) {
+ for (String apk : apks) {
if (apkPinSizeLimit <= 0) {
Slog.w(TAG, "Reached to the pin size limit. Skipping: " + apk);
// Continue instead of break to print all skipped APK names.
continue;
}
- PinnedFile pf = mInjector.pinFileInternal(apk, apkPinSizeLimit, /*attemptPinIntrospection=*/true);
+ String pinGroup = getNameForKey(key);
+ boolean shouldPinDeps = apk.equals(appInfo.sourceDir);
+ PinnedFile pf = pinFile(apk, apkPinSizeLimit, appInfo, pinGroup, shouldPinDeps);
if (pf == null) {
Slog.e(TAG, "Failed to pin " + apk);
continue;
}
- pf.groupName = getNameForKey(key);
if (DEBUG) {
Slog.i(TAG, "Pinned " + pf.fileName);
}
synchronized (this) {
pinnedApp.mFiles.add(pf);
- mPinnedFiles.put(pf.fileName, pf);
}
apkPinSizeLimit -= pf.bytesPinned;
- if (apk.equals(appInfo.sourceDir) && !shouldSkipArtPins) {
- pinOptimizedDexDependencies(pf, Integer.MAX_VALUE, appInfo);
- }
}
}
@@ -892,19 +893,23 @@
* that related to the file but not within itself.
*
* @param fileToPin File to pin
- * @param maxBytesToPin maximum quota allowed for pinning
- * @return total bytes that were pinned.
+ * @param bytesRequestedToPin maximum bytes requested to pin for {@code fileToPin}.
+ * @param pinOptimizedDeps whether optimized dependencies such as odex,vdex, etc be pinned.
+ * Note: {@code bytesRequestedToPin} limit will not apply to optimized
+ * dependencies pinned, only global quotas will apply instead.
+ * @return pinned file
*/
- public int pinFile(String fileToPin, int maxBytesToPin, @Nullable ApplicationInfo appInfo,
- @Nullable String groupName) {
+ public PinnedFile pinFile(String fileToPin, long bytesRequestedToPin,
+ @Nullable ApplicationInfo appInfo, @Nullable String groupName,
+ boolean pinOptimizedDeps) {
PinnedFile existingPin;
- synchronized(this) {
+ synchronized (this) {
existingPin = mPinnedFiles.get(fileToPin);
}
if (existingPin != null) {
- if (existingPin.bytesPinned == maxBytesToPin) {
+ if (existingPin.bytesPinned == bytesRequestedToPin) {
// Duplicate pin requesting same amount of bytes, lets just bail out.
- return 0;
+ return null;
} else {
// User decided to pin a different amount of bytes than currently pinned
// so this is a valid pin request. Unpin the previous version before repining.
@@ -915,26 +920,38 @@
}
}
+ long remainingQuota = getAvailableGlobalQuota();
+
+ if (pinGlobalQuota()) {
+ if (remainingQuota <= 0) {
+ Slog.w(TAG, "Reached pin quota, skipping file: " + fileToPin);
+ return null;
+ }
+ bytesRequestedToPin = Math.min(bytesRequestedToPin, remainingQuota);
+ }
+
boolean isApk = fileToPin.endsWith(".apk");
- int bytesPinned = 0;
- PinnedFile pf = mInjector.pinFileInternal(fileToPin, maxBytesToPin,
+
+ PinnedFile pf = mInjector.pinFileInternal(this, fileToPin, bytesRequestedToPin,
/*attemptPinIntrospection=*/isApk);
if (pf == null) {
Slog.e(TAG, "Failed to pin file = " + fileToPin);
- return 0;
+ return null;
}
pf.groupName = groupName != null ? groupName : "";
- bytesPinned += pf.bytesPinned;
- maxBytesToPin -= bytesPinned;
+ mCurrentPinnedMemory += pf.bytesPinned;
synchronized (this) {
mPinnedFiles.put(pf.fileName, pf);
}
- if (maxBytesToPin > 0) {
- pinOptimizedDexDependencies(pf, maxBytesToPin, appInfo);
+
+ if (pinOptimizedDeps) {
+ mCurrentPinnedMemory +=
+ pinOptimizedDexDependencies(pf, getAvailableGlobalQuota(), appInfo);
}
- return bytesPinned;
+
+ return pf;
}
/**
@@ -945,13 +962,13 @@
* to null it will use the default supported ABI by the device.
* @return total bytes pinned.
*/
- private int pinOptimizedDexDependencies(
- PinnedFile pinnedFile, int maxBytesToPin, @Nullable ApplicationInfo appInfo) {
+ private long pinOptimizedDexDependencies(
+ PinnedFile pinnedFile, long maxBytesToPin, @Nullable ApplicationInfo appInfo) {
if (pinnedFile == null) {
return 0;
}
- int bytesPinned = 0;
+ long bytesPinned = 0;
if (pinnedFile.fileName.endsWith(".jar") | pinnedFile.fileName.endsWith(".apk")) {
String abi = null;
if (appInfo != null) {
@@ -974,7 +991,7 @@
// Unpin if it was already pinned prior to re-pinning.
unpinFile(file);
- PinnedFile df = mInjector.pinFileInternal(file, maxBytesToPin,
+ PinnedFile df = mInjector.pinFileInternal(this, file, maxBytesToPin,
/*attemptPinIntrospection=*/false);
if (df == null) {
Slog.i(TAG, "Failed to pin ART file = " + file);
@@ -992,7 +1009,8 @@
return bytesPinned;
}
- /** mlock length bytes of fileToPin in memory
+ /**
+ * mlock length bytes of fileToPin in memory
*
* If attemptPinIntrospection is true, then treat the file to pin as a zip file and
* look for a "pinlist.meta" file in the archive root directory. The structure of this
@@ -1029,8 +1047,8 @@
* zip in order to extract the
* @return Pinned memory resource owner thing or null on error
*/
- private static PinnedFile pinFileInternal(
- String fileToPin, int maxBytesToPin, boolean attemptPinIntrospection) {
+ private PinnedFile pinFileInternal(
+ String fileToPin, long maxBytesToPin, boolean attemptPinIntrospection) {
if (DEBUG) {
Slog.d(TAG, "pin file: " + fileToPin + " use-pinlist: " + attemptPinIntrospection);
}
@@ -1054,8 +1072,8 @@
}
return pinnedFile;
} finally {
- safeClose(pinRangeStream);
- safeClose(fileAsZip); // Also closes any streams we've opened
+ PinnerUtils.safeClose(pinRangeStream);
+ PinnerUtils.safeClose(fileAsZip); // Also closes any streams we've opened
}
}
@@ -1068,11 +1086,8 @@
try {
zip = new ZipFile(fileName);
} catch (IOException ex) {
- Slog.w(TAG,
- String.format(
- "could not open \"%s\" as zip: pinning as blob",
- fileName),
- ex);
+ Slog.w(TAG, String.format("could not open \"%s\" as zip: pinning as blob", fileName),
+ ex);
}
return zip;
}
@@ -1112,9 +1127,9 @@
pinMetaStream = zipFile.getInputStream(pinMetaEntry);
} catch (IOException ex) {
Slog.w(TAG,
- String.format("error reading pin metadata \"%s\": pinning as blob",
- fileName),
- ex);
+ String.format(
+ "error reading pin metadata \"%s\": pinning as blob", fileName),
+ ex);
}
} else {
Slog.w(TAG,
@@ -1124,57 +1139,6 @@
return pinMetaStream;
}
- private static abstract class PinRangeSource {
- /** Retrive a range to pin.
- *
- * @param outPinRange Receives the pin region
- * @return True if we filled in outPinRange or false if we're out of pin entries
- */
- abstract boolean read(PinRange outPinRange);
- }
-
- private static final class PinRangeSourceStatic extends PinRangeSource {
- private final int mPinStart;
- private final int mPinLength;
- private boolean mDone = false;
-
- PinRangeSourceStatic(int pinStart, int pinLength) {
- mPinStart = pinStart;
- mPinLength = pinLength;
- }
-
- @Override
- boolean read(PinRange outPinRange) {
- outPinRange.start = mPinStart;
- outPinRange.length = mPinLength;
- boolean done = mDone;
- mDone = true;
- return !done;
- }
- }
-
- private static final class PinRangeSourceStream extends PinRangeSource {
- private final DataInputStream mStream;
- private boolean mDone = false;
-
- PinRangeSourceStream(InputStream stream) {
- mStream = new DataInputStream(stream);
- }
-
- @Override
- boolean read(PinRange outPinRange) {
- if (!mDone) {
- try {
- outPinRange.start = mStream.readInt();
- outPinRange.length = mStream.readInt();
- } catch (IOException ex) {
- mDone = true;
- }
- }
- return !mDone;
- }
- }
-
/**
* Helper for pinFile.
*
@@ -1185,25 +1149,20 @@
* @return PinnedFile or null on error
*/
private static PinnedFile pinFileRanges(
- String fileToPin,
- int maxBytesToPin,
- PinRangeSource pinRangeSource)
- {
+ String fileToPin, long maxBytesToPin, PinRangeSource pinRangeSource) {
FileDescriptor fd = new FileDescriptor();
long address = -1;
- int mapSize = 0;
+ long mapSize = 0;
try {
int openFlags = (OsConstants.O_RDONLY | OsConstants.O_CLOEXEC);
fd = Os.open(fileToPin, openFlags, 0);
mapSize = (int) Math.min(Os.fstat(fd).st_size, Integer.MAX_VALUE);
- address = Os.mmap(0, mapSize,
- OsConstants.PROT_READ,
- OsConstants.MAP_SHARED,
- fd, /*offset=*/0);
+ address = Os.mmap(
+ 0, mapSize, OsConstants.PROT_READ, OsConstants.MAP_SHARED, fd, /*offset=*/0);
PinRange pinRange = new PinRange();
- int bytesPinned = 0;
+ long bytesPinned = 0;
// We pin at page granularity, so make sure the limit is page-aligned
if (maxBytesToPin % PAGE_SIZE != 0) {
@@ -1211,10 +1170,10 @@
}
while (bytesPinned < maxBytesToPin && pinRangeSource.read(pinRange)) {
- int pinStart = pinRange.start;
- int pinLength = pinRange.length;
- pinStart = clamp(0, pinStart, mapSize);
- pinLength = clamp(0, pinLength, mapSize - pinStart);
+ long pinStart = pinRange.start;
+ long pinLength = pinRange.length;
+ pinStart = PinnerUtils.clamp(0, pinStart, mapSize);
+ pinLength = PinnerUtils.clamp(0, pinLength, mapSize - pinStart);
pinLength = Math.min(maxBytesToPin - bytesPinned, pinLength);
// mlock doesn't require the region to be page-aligned, but we snap the
@@ -1229,14 +1188,13 @@
if (pinLength % PAGE_SIZE != 0) {
pinLength += PAGE_SIZE - pinLength % PAGE_SIZE;
}
- pinLength = clamp(0, pinLength, maxBytesToPin - bytesPinned);
+ pinLength = PinnerUtils.clamp(0, pinLength, maxBytesToPin - bytesPinned);
if (pinLength > 0) {
if (DEBUG) {
Slog.d(TAG,
- String.format(
- "pinning at %s %s bytes of %s",
- pinStart, pinLength, fileToPin));
+ String.format("pinning at %s %s bytes of %s", pinStart, pinLength,
+ fileToPin));
}
Os.mlock(address + pinStart, pinLength);
}
@@ -1244,15 +1202,15 @@
}
PinnedFile pinnedFile = new PinnedFile(address, mapSize, fileToPin, bytesPinned);
- address = -1; // Ownership transferred
+ address = -1; // Ownership transferred
return pinnedFile;
} catch (ErrnoException ex) {
Slog.e(TAG, "Could not pin file " + fileToPin, ex);
return null;
} finally {
- safeClose(fd);
+ PinnerUtils.safeClose(fd);
if (address >= 0) {
- safeMunmap(address, mapSize);
+ PinnerUtils.safeMunmap(address, mapSize);
}
}
}
@@ -1273,81 +1231,50 @@
}
}
- public void unpinFile(String filename) {
+ /**
+ * Unpin a file and its optimized dependencies.
+ *
+ * @param filename file to unpin.
+ * @return number of bytes unpinned, 0 in case of failure or nothing to unpin.
+ */
+ public long unpinFile(String filename) {
PinnedFile pinnedFile;
synchronized (this) {
pinnedFile = mPinnedFiles.get(filename);
}
if (pinnedFile == null) {
// File not pinned, nothing to do.
- return;
+ return 0;
}
+ long unpinnedBytes = pinnedFile.bytesPinned;
pinnedFile.close();
synchronized (this) {
if (DEBUG) {
Slog.d(TAG, "Unpinned file: " + filename);
}
+ mCurrentPinnedMemory -= pinnedFile.bytesPinned;
+
mPinnedFiles.remove(pinnedFile.fileName);
for (PinnedFile dep : pinnedFile.pinnedDeps) {
if (dep == null) {
continue;
}
+ unpinnedBytes -= dep.bytesPinned;
+ mCurrentPinnedMemory -= dep.bytesPinned;
mPinnedFiles.remove(dep.fileName);
if (DEBUG) {
Slog.d(TAG, "Unpinned dependency: " + dep.fileName);
}
}
}
- }
- private static int clamp(int min, int value, int max) {
- return Math.max(min, Math.min(value, max));
- }
-
- private static void safeMunmap(long address, long mapSize) {
- try {
- Os.munmap(address, mapSize);
- } catch (ErrnoException ex) {
- Slog.w(TAG, "ignoring error in unmap", ex);
- }
- }
-
- /**
- * Close FD, swallowing irrelevant errors.
- */
- private static void safeClose(@Nullable FileDescriptor fd) {
- if (fd != null && fd.valid()) {
- try {
- Os.close(fd);
- } catch (ErrnoException ex) {
- // Swallow the exception: non-EBADF errors in close(2)
- // indicate deferred paging write errors, which we
- // don't care about here. The underlying file
- // descriptor is always closed.
- if (ex.errno == OsConstants.EBADF) {
- throw new AssertionError(ex);
- }
- }
- }
- }
-
- /**
- * Close closeable thing, swallowing errors.
- */
- private static void safeClose(@Nullable Closeable thing) {
- if (thing != null) {
- try {
- thing.close();
- } catch (IOException ex) {
- Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
- }
- }
+ return unpinnedBytes;
}
public List<PinnedFileStat> getPinnerStats() {
ArrayList<PinnedFileStat> stats = new ArrayList<>();
Collection<PinnedFile> pinnedFiles;
- synchronized(this) {
+ synchronized (this) {
pinnedFiles = mPinnedFiles.values();
}
for (PinnedFile pf : pinnedFiles) {
@@ -1355,8 +1282,8 @@
stats.add(stat);
}
if (mCurrentlyPinnedAnonSize > 0) {
- stats.add(new PinnedFileStat(ANON_REGION_STAT_NAME,
- mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME));
+ stats.add(new PinnedFileStat(
+ ANON_REGION_STAT_NAME, mCurrentlyPinnedAnonSize, ANON_REGION_STAT_NAME));
}
return stats;
}
@@ -1364,71 +1291,124 @@
public final class BinderService extends IPinnerService.Stub {
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, pw))
+ return;
HashSet<PinnedFile> shownPins = new HashSet<>();
- HashSet<String> groups = new HashSet<>();
- final int bytesPerMB = 1024 * 1024;
+ HashSet<String> shownGroups = new HashSet<>();
+ HashSet<String> groupsToPrint = new HashSet<>();
+ final double bytesPerMB = 1024 * 1024;
+ pw.format("Pinner Configs:\n");
+ pw.format(" Total Pinner quota: %d%% of total device memory\n",
+ mConfiguredMaxPinnedMemoryPercentage);
+ pw.format(" Maximum Pinner quota: %d bytes (%.2f MB)\n", mConfiguredMaxPinnedMemory,
+ mConfiguredMaxPinnedMemory / bytesPerMB);
+ pw.format(" Max Home App Pin Bytes (without deps): %d\n", mConfiguredHomePinBytes);
+ pw.format("\nPinned Files:\n");
synchronized (PinnerService.this) {
long totalSize = 0;
+
+ // We print apps separately from regular pins as they contain extra information that
+ // other pins do not.
for (int key : mPinnedApps.keySet()) {
PinnedApp app = mPinnedApps.get(key);
pw.print(getNameForKey(key));
- pw.print(" uid="); pw.print(app.uid);
- pw.print(" active="); pw.print(app.active);
+ pw.print(" uid=");
+ pw.print(app.uid);
+ pw.print(" active=");
+ pw.print(app.active);
+
+ if (!app.mFiles.isEmpty()) {
+ shownGroups.add(app.mFiles.getFirst().groupName);
+ }
pw.println();
+ long bytesPinnedForApp = 0;
+ long bytesPinnedForAppDeps = 0;
for (PinnedFile pf : mPinnedApps.get(key).mFiles) {
pw.print(" ");
- pw.format("%s pinned:%d bytes (%d MB) pinlist:%b\n", pf.fileName,
+ pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b\n", pf.fileName,
pf.bytesPinned, pf.bytesPinned / bytesPerMB, pf.used_pinlist);
totalSize += pf.bytesPinned;
+ bytesPinnedForApp += pf.bytesPinned;
shownPins.add(pf);
for (PinnedFile dep : pf.pinnedDeps) {
pw.print(" ");
- pw.format("%s pinned:%d bytes (%d MB) pinlist:%b (Dependency)\n", dep.fileName,
- dep.bytesPinned, dep.bytesPinned / bytesPerMB, dep.used_pinlist);
+ pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n",
+ dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB,
+ dep.used_pinlist);
totalSize += dep.bytesPinned;
+ bytesPinnedForAppDeps += dep.bytesPinned;
shownPins.add(dep);
}
}
+ long bytesPinnedForAppAndDeps = bytesPinnedForApp + bytesPinnedForAppDeps;
+ pw.format("Total Pinned = %d (%.2f MB) [App=%d (%.2f MB), "
+ + "Dependencies=%d (%.2f MB)]\n\n",
+ bytesPinnedForAppAndDeps, bytesPinnedForAppAndDeps / bytesPerMB,
+ bytesPinnedForApp, bytesPinnedForApp / bytesPerMB,
+ bytesPinnedForAppDeps, bytesPinnedForAppDeps / bytesPerMB);
}
pw.println();
for (PinnedFile pinnedFile : mPinnedFiles.values()) {
- if (!groups.contains(pinnedFile.groupName)) {
- groups.add(pinnedFile.groupName);
+ if (!groupsToPrint.contains(pinnedFile.groupName)
+ && !shownGroups.contains(pinnedFile.groupName)) {
+ groupsToPrint.add(pinnedFile.groupName);
}
}
- boolean firstPinInGroup = true;
- for (String group : groups) {
+
+ // Print all the non app groups.
+ for (String group : groupsToPrint) {
List<PinnedFile> groupPins = getAllPinsForGroup(group);
+ pw.print("\nGroup:" + group);
+ long bytesPinnedForGroupNoDeps = 0;
+ long bytesPinnedForGroupDeps = 0;
+ pw.println();
for (PinnedFile pinnedFile : groupPins) {
if (shownPins.contains(pinnedFile)) {
- // Already showed in the dump and accounted for, skip.
+ // Already displayed and accounted for, skip.
continue;
}
- if (firstPinInGroup) {
- firstPinInGroup = false;
- // Ensure we only print when there are pins for groups not yet shown
- // in the pinned app section.
- pw.print("Group:" + group);
- pw.println();
- }
- pw.format(" %s pinned:%d bytes (%d MB) pinlist:%b\n", pinnedFile.fileName,
- pinnedFile.bytesPinned, pinnedFile.bytesPinned / bytesPerMB,
- pinnedFile.used_pinlist);
+ pw.format(" %s pinned: %d bytes (%.2f MB) pinlist:%b\n",
+ pinnedFile.fileName, pinnedFile.bytesPinned,
+ pinnedFile.bytesPinned / bytesPerMB, pinnedFile.used_pinlist);
totalSize += pinnedFile.bytesPinned;
+ bytesPinnedForGroupNoDeps += pinnedFile.bytesPinned;
+ shownPins.add(pinnedFile);
+ for (PinnedFile dep : pinnedFile.pinnedDeps) {
+ if (shownPins.contains(dep)) {
+ // Already displayed and accounted for, skip.
+ continue;
+ }
+ pw.print(" ");
+ pw.format("%s pinned:%d bytes (%.2f MB) pinlist:%b (Dependency)\n",
+ dep.fileName, dep.bytesPinned, dep.bytesPinned / bytesPerMB,
+ dep.used_pinlist);
+ totalSize += dep.bytesPinned;
+ bytesPinnedForGroupDeps += dep.bytesPinned;
+ shownPins.add(dep);
+ }
}
+ long bytesPinnedForGroup = bytesPinnedForGroupNoDeps + bytesPinnedForGroupDeps;
+ pw.format("Total Pinned = %d (%.2f MB) [Main=%d (%.2f MB), "
+ + "Dependencies=%d (%.2f MB)]\n\n",
+ bytesPinnedForGroup, bytesPinnedForGroup / bytesPerMB,
+ bytesPinnedForGroupNoDeps, bytesPinnedForGroupNoDeps / bytesPerMB,
+ bytesPinnedForGroupDeps, bytesPinnedForGroupDeps / bytesPerMB);
}
pw.println();
if (mPinAnonAddress != 0) {
- pw.format("Pinned anon region: %d (%d MB)\n", mCurrentlyPinnedAnonSize, mCurrentlyPinnedAnonSize / bytesPerMB);
+ pw.format("Pinned anon region: %d (%.2f MB)\n", mCurrentlyPinnedAnonSize,
+ mCurrentlyPinnedAnonSize / bytesPerMB);
totalSize += mCurrentlyPinnedAnonSize;
}
- pw.format("Total pinned: %s bytes (%s MB)\n", totalSize, totalSize / bytesPerMB);
+ pw.format("Total pinned: %d bytes (%.2f MB)\n", totalSize, totalSize / bytesPerMB);
+ pw.format("Available Pinner quota: %d bytes (%.2f MB)\n", getAvailableGlobalQuota(),
+ getAvailableGlobalQuota() / bytesPerMB);
pw.println();
if (!mPendingRepin.isEmpty()) {
pw.print("Pending repin: ");
for (int key : mPendingRepin.values()) {
- pw.print(getNameForKey(key)); pw.print(' ');
+ pw.print(getNameForKey(key));
+ pw.print(' ');
}
pw.println();
}
@@ -1462,8 +1442,9 @@
repin();
break;
default:
- printError(out, String.format(
- "Unknown pinner command: %s. Supported commands: repin", command));
+ printError(out,
+ String.format("Unknown pinner command: %s. Supported commands: repin",
+ command));
resultReceiver.send(-1, null);
return;
}
@@ -1479,46 +1460,6 @@
}
}
- @VisibleForTesting
- public static final class PinnedFile implements AutoCloseable {
- private long mAddress;
- final int mapSize;
- final String fileName;
- final int bytesPinned;
-
- // Whether this file was pinned using a pinlist
- boolean used_pinlist;
-
- // User defined group name for pinner accounting
- String groupName = "";
- ArrayList<PinnedFile> pinnedDeps = new ArrayList<>();
-
- PinnedFile(long address, int mapSize, String fileName, int bytesPinned) {
- mAddress = address;
- this.mapSize = mapSize;
- this.fileName = fileName;
- this.bytesPinned = bytesPinned;
- }
-
- @Override
- public void close() {
- if (mAddress >= 0) {
- safeMunmap(mAddress, mapSize);
- mAddress = -1;
- }
- for (PinnedFile dep : pinnedDeps) {
- if (dep != null) {
- dep.close();
- }
- }
- }
-
- @Override
- public void finalize() {
- close();
- }
- }
-
final static class PinRange {
int start;
int length;
@@ -1528,7 +1469,6 @@
* Represents an app that was pinned.
*/
private final class PinnedApp {
-
/**
* The uid of the package being pinned. This stays constant while the package stays
* installed.
@@ -1557,11 +1497,9 @@
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
- case PIN_ONSTART_MSG:
- {
+ case PIN_ONSTART_MSG: {
handlePinOnStart();
- }
- break;
+ } break;
default:
super.handleMessage(msg);
diff --git a/services/core/java/com/android/server/pinner/PinnerUtils.java b/services/core/java/com/android/server/pinner/PinnerUtils.java
new file mode 100644
index 0000000..a836a83
--- /dev/null
+++ b/services/core/java/com/android/server/pinner/PinnerUtils.java
@@ -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.server.pinner;
+
+import android.annotation.Nullable;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Slog;
+
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/* package */ final class PinnerUtils {
+ private static final String TAG = "PinnerUtils";
+
+ public static long clamp(long min, long value, long max) {
+ return Math.max(min, Math.min(value, max));
+ }
+
+ public static void safeMunmap(long address, long mapSize) {
+ try {
+ Os.munmap(address, mapSize);
+ } catch (ErrnoException ex) {
+ Slog.w(TAG, "ignoring error in unmap", ex);
+ }
+ }
+
+ /**
+ * Close FD, swallowing irrelevant errors.
+ */
+ public static void safeClose(@Nullable FileDescriptor fd) {
+ if (fd != null && fd.valid()) {
+ try {
+ Os.close(fd);
+ } catch (ErrnoException ex) {
+ // Swallow the exception: non-EBADF errors in close(2)
+ // indicate deferred paging write errors, which we
+ // don't care about here. The underlying file
+ // descriptor is always closed.
+ if (ex.errno == OsConstants.EBADF) {
+ throw new AssertionError(ex);
+ }
+ }
+ }
+ }
+
+ /**
+ * Close closeable thing, swallowing errors.
+ */
+ public static void safeClose(@Nullable Closeable thing) {
+ if (thing != null) {
+ try {
+ thing.close();
+ } catch (IOException ex) {
+ Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 4665a72..89ced12 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -2208,10 +2208,10 @@
return true;
}
boolean permissionGranted = requireFullPermission ? hasPermission(
- Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
: (hasPermission(
- android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
- || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS));
+ android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingUid)
+ || hasPermission(Manifest.permission.INTERACT_ACROSS_USERS, callingUid));
if (!permissionGranted) {
if (Process.isIsolatedUid(callingUid) && isKnownIsolatedComputeApp(callingUid)) {
return checkIsolatedOwnerHasPermission(callingUid, requireFullPermission);
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 9ecc7b9..1569fa0 100644
--- a/services/core/java/com/android/server/pm/DexOptHelper.java
+++ b/services/core/java/com/android/server/pm/DexOptHelper.java
@@ -70,13 +70,13 @@
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
-import com.android.server.PinnerService;
import com.android.server.art.ArtManagerLocal;
import com.android.server.art.DexUseManagerLocal;
import com.android.server.art.ReasonMapping;
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.DexoptParams;
import com.android.server.art.model.DexoptResult;
+import com.android.server.pinner.PinnerService;
import com.android.server.pm.PackageDexOptimizer.DexOptResult;
import com.android.server.pm.dex.DexManager;
import com.android.server.pm.dex.DexoptOptions;
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index f449126..aca65bf 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1019,7 +1019,9 @@
&& scanInstallPackages(requests, createdAppId, versionInfos)) {
List<ReconciledPackage> reconciledPackages =
reconcileInstallPackages(requests, versionInfos);
- if (reconciledPackages != null && commitInstallPackages(reconciledPackages)) {
+ if (reconciledPackages != null
+ && renameAndUpdatePaths(requests)
+ && commitInstallPackages(reconciledPackages)) {
success = true;
}
}
@@ -1029,24 +1031,49 @@
}
}
- private boolean prepareInstallPackages(List<InstallRequest> requests) {
- // TODO: will remove the locking after doRename is moved out of prepare
+ private boolean renameAndUpdatePaths(List<InstallRequest> requests) {
try (PackageManagerTracedLock installLock = mPm.mInstallLock.acquireLock()) {
for (InstallRequest request : requests) {
- try {
- Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
- request.onPrepareStarted();
- preparePackageLI(request);
- } catch (PrepareFailure prepareFailure) {
- request.setError(prepareFailure.error,
- prepareFailure.getMessage());
- request.setOriginPackage(prepareFailure.mConflictingPackage);
- request.setOriginPermission(prepareFailure.mConflictingPermission);
- return false;
- } finally {
- request.onPrepareFinished();
- Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ ParsedPackage parsedPackage = request.getParsedPackage();
+ final boolean isApex = (request.getScanFlags() & SCAN_AS_APEX) != 0;
+ if (isApex) {
+ continue;
}
+ try {
+ doRenameLI(request, parsedPackage);
+ setUpFsVerity(parsedPackage);
+ } catch (Installer.InstallerException | IOException | DigestException
+ | NoSuchAlgorithmException | PrepareFailure e) {
+ request.setError(PackageManagerException.INTERNAL_ERROR_VERITY_SETUP,
+ "Failed to set up verity: " + e);
+ return false;
+ }
+
+ // update paths that are set before renaming
+ PackageSetting scannedPackageSetting = request.getScannedPackageSetting();
+ scannedPackageSetting.setPath(new File(parsedPackage.getPath()));
+ scannedPackageSetting.setLegacyNativeLibraryPath(
+ parsedPackage.getNativeLibraryRootDir());
+ }
+ return true;
+ }
+ }
+
+ private boolean prepareInstallPackages(List<InstallRequest> requests) {
+ for (InstallRequest request : requests) {
+ try {
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
+ request.onPrepareStarted();
+ preparePackage(request);
+ } catch (PrepareFailure prepareFailure) {
+ request.setError(prepareFailure.error,
+ prepareFailure.getMessage());
+ request.setOriginPackage(prepareFailure.mConflictingPackage);
+ request.setOriginPermission(prepareFailure.mConflictingPermission);
+ return false;
+ } finally {
+ request.onPrepareFinished();
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}
return true;
@@ -1231,8 +1258,7 @@
return newProp != null && newProp.getBoolean();
}
- @GuardedBy("mPm.mInstallLock")
- private void preparePackageLI(InstallRequest request) throws PrepareFailure {
+ private void preparePackage(InstallRequest request) throws PrepareFailure {
final int[] allUsers = mPm.mUserManager.getUserIds();
final int installFlags = request.getInstallFlags();
final boolean onExternal = request.getVolumeUuid() != null;
@@ -1739,18 +1765,7 @@
}
}
- if (!isApex) {
- doRenameLI(request, parsedPackage);
-
- try {
- setUpFsVerity(parsedPackage);
- } catch (Installer.InstallerException | IOException | DigestException
- | NoSuchAlgorithmException e) {
- throw PrepareFailure.ofInternalError(
- "Failed to set up verity: " + e,
- PackageManagerException.INTERNAL_ERROR_VERITY_SETUP);
- }
- } else {
+ if (isApex) {
// Use the path returned by apexd
parsedPackage.setPath(request.getApexInfo().modulePath);
parsedPackage.setBaseApkPath(request.getApexInfo().modulePath);
@@ -2092,7 +2107,21 @@
// Reflect the rename in scanned details
try {
- parsedPackage.setPath(afterCodeFile.getCanonicalPath());
+ String afterCanonicalPath = afterCodeFile.getCanonicalPath();
+ String beforeCanonicalPath = beforeCodeFile.getCanonicalPath();
+ parsedPackage.setPath(afterCanonicalPath);
+
+ parsedPackage.setNativeLibraryDir(
+ parsedPackage.getNativeLibraryDir()
+ .replace(beforeCanonicalPath, afterCanonicalPath));
+ parsedPackage.setNativeLibraryRootDir(
+ parsedPackage.getNativeLibraryRootDir()
+ .replace(beforeCanonicalPath, afterCanonicalPath));
+ String secondaryNativeLibraryDir = parsedPackage.getSecondaryNativeLibraryDir();
+ if (secondaryNativeLibraryDir != null) {
+ parsedPackage.setSecondaryNativeLibraryDir(
+ secondaryNativeLibraryDir.replace(beforeCanonicalPath, afterCanonicalPath));
+ }
} catch (IOException e) {
Slog.e(TAG, "Failed to get path: " + afterCodeFile, e);
throw new PrepareFailure(PackageManager.INSTALL_FAILED_MEDIA_UNAVAILABLE,
@@ -2102,6 +2131,7 @@
afterCodeFile, parsedPackage.getBaseApkPath()));
parsedPackage.setSplitCodePaths(FileUtils.rewriteAfterRename(beforeCodeFile,
afterCodeFile, parsedPackage.getSplitCodePaths()));
+ request.updateAllCodePaths(AndroidPackageUtils.getAllCodePaths(parsedPackage));
}
// TODO(b/168126411): Once staged install flow starts using the same folder as non-staged
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index ae7749b..1f79ac0 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -615,6 +615,20 @@
return mScanResult.mDynamicSharedLibraryInfos;
}
+ public void updateAllCodePaths(List<String> paths) {
+ if (mScanResult.mSdkSharedLibraryInfo != null) {
+ mScanResult.mSdkSharedLibraryInfo.setAllCodePaths(paths);
+ }
+ if (mScanResult.mStaticSharedLibraryInfo != null) {
+ mScanResult.mStaticSharedLibraryInfo.setAllCodePaths(paths);
+ }
+ if (mScanResult.mDynamicSharedLibraryInfos != null) {
+ for (SharedLibraryInfo info : mScanResult.mDynamicSharedLibraryInfos) {
+ info.setAllCodePaths(paths);
+ }
+ }
+ }
+
@Nullable
public PackageSetting getScannedPackageSetting() {
assertScanResultExists();
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 5653da0..8657de2 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -716,7 +716,7 @@
visiblePackages.add(info.getActivityInfo().packageName);
}
final List<ApplicationInfo> installedPackages =
- mPackageManagerInternal.getInstalledApplications(
+ mPackageManagerInternal.getInstalledApplicationsCrossUser(
/* flags= */ 0, user.getIdentifier(), callingUid);
for (ApplicationInfo applicationInfo : installedPackages) {
if (!visiblePackages.contains(applicationInfo.packageName)) {
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 1316df1..b1b1637 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -50,6 +50,7 @@
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManagerInternal;
+import android.app.role.RoleManager;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
@@ -201,6 +202,9 @@
Manifest.permission.USE_FULL_SCREEN_INTENT
);
+ private static final String ROLE_SYSTEM_APP_PROTECTION_SERVICE =
+ "android.app.role.SYSTEM_APP_PROTECTION_SERVICE";
+
final PackageArchiver mPackageArchiver;
private final Context mContext;
@@ -1454,6 +1458,12 @@
.createEvent(DevicePolicyEnums.UNINSTALL_PACKAGE)
.setAdmin(callerPackageName)
.write();
+ } else if (isSystemAppProtectionRoleHolder(snapshot, userId, callingUid)) {
+ // Allow the SYSTEM_APP_PROTECTION_SERVICE role holder to silently uninstall, with a
+ // clean calling identity to get DELETE_PACKAGES permission
+ Binder.withCleanCallingIdentity(() ->
+ mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags)
+ );
} else {
ApplicationInfo appInfo = snapshot.getApplicationInfo(callerPackageName, 0, userId);
if (appInfo.targetSdkVersion >= Build.VERSION_CODES.P) {
@@ -1475,6 +1485,29 @@
}
}
+ private Boolean isSystemAppProtectionRoleHolder(
+ @NonNull Computer snapshot, int userId, int callingUid) {
+ if (!Flags.deletePackagesSilentlyBackport()) {
+ return false;
+ }
+ String holderPackageName = Binder.withCleanCallingIdentity(() -> {
+ RoleManager roleManager = mPm.mContext.getSystemService(RoleManager.class);
+ if (roleManager == null) {
+ return null;
+ }
+ List<String> holders = roleManager.getRoleHoldersAsUser(
+ ROLE_SYSTEM_APP_PROTECTION_SERVICE, UserHandle.of(userId));
+ if (holders.isEmpty()) {
+ return null;
+ }
+ return holders.get(0);
+ });
+ if (holderPackageName == null) {
+ return false;
+ }
+ return snapshot.getPackageUid(holderPackageName, /* flags= */ 0, userId) == callingUid;
+ }
+
@Override
public void uninstallExistingPackage(VersionedPackage versionedPackage,
String callerPackageName, IntentSender statusReceiver, int userId) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index ff9c3e5..611e0d8 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -3389,18 +3389,31 @@
return true;
}
// Does it contain a device admin for any user?
- int[] users;
+ int[] allUsers = mUserManager.getUserIds();
+ int[] targetUsers;
if (userId == UserHandle.USER_ALL) {
- users = mUserManager.getUserIds();
+ targetUsers = allUsers;
} else {
- users = new int[]{userId};
+ targetUsers = new int[]{userId};
}
- for (int i = 0; i < users.length; ++i) {
- if (dpm.packageHasActiveAdmins(packageName, users[i])) {
+
+ for (int i = 0; i < targetUsers.length; ++i) {
+ if (dpm.packageHasActiveAdmins(packageName, targetUsers[i])) {
return true;
}
- if (isDeviceManagementRoleHolder(packageName, users[i])
- && dpmi.isUserOrganizationManaged(users[i])) {
+ }
+
+ // If a package is DMRH on a managed user, it should also be treated as an admin on
+ // that user. If that package is also a system package, it should also be protected
+ // on other users otherwise "uninstall updates" on an unmanaged user may break
+ // management on other users because apk version is shared between all users.
+ var packageState = snapshotComputer().getPackageStateInternal(packageName);
+ if (packageState == null) {
+ return false;
+ }
+ for (int user : packageState.isSystem() ? allUsers : targetUsers) {
+ if (isDeviceManagementRoleHolder(packageName, user)
+ && dpmi.isUserOrganizationManaged(user)) {
return true;
}
}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index df9f7fb..5fc3e33 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -1015,8 +1015,7 @@
permission, attributionSource, message, forDataDelivery, startDataDelivery,
fromDatasource, attributedOp);
// Finish any started op if some step in the attribution chain failed.
- if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED
- && result != PermissionChecker.PERMISSION_SOFT_DENIED) {
+ if (startDataDelivery && result != PermissionChecker.PERMISSION_GRANTED) {
if (attributedOp == AppOpsManager.OP_NONE) {
finishDataDelivery(AppOpsManager.permissionToOpCode(permission),
attributionSource.asState(), fromDatasource);
diff --git a/services/core/java/com/android/server/policy/KeyCombinationManager.java b/services/core/java/com/android/server/policy/KeyCombinationManager.java
index 9dfaca8..1592ef3 100644
--- a/services/core/java/com/android/server/policy/KeyCombinationManager.java
+++ b/services/core/java/com/android/server/policy/KeyCombinationManager.java
@@ -65,21 +65,21 @@
* };
* </pre>
*/
- abstract static class TwoKeysCombinationRule {
+ public abstract static class TwoKeysCombinationRule {
private int mKeyCode1;
private int mKeyCode2;
- TwoKeysCombinationRule(int keyCode1, int keyCode2) {
+ public TwoKeysCombinationRule(int keyCode1, int keyCode2) {
mKeyCode1 = keyCode1;
mKeyCode2 = keyCode2;
}
- boolean preCondition() {
+ public boolean preCondition() {
return true;
}
boolean shouldInterceptKey(int keyCode) {
- return preCondition() && (keyCode == mKeyCode1 || keyCode == mKeyCode2);
+ return (keyCode == mKeyCode1 || keyCode == mKeyCode2) && preCondition();
}
boolean shouldInterceptKeys(SparseLongArray downTimes) {
@@ -94,12 +94,12 @@
}
// The excessive delay before it dispatching to client.
- long getKeyInterceptDelayMs() {
+ public long getKeyInterceptDelayMs() {
return COMBINE_KEY_DELAY_MILLIS;
}
- abstract void execute();
- abstract void cancel();
+ public abstract void execute();
+ public abstract void cancel();
@Override
public String toString() {
@@ -128,18 +128,18 @@
}
}
- KeyCombinationManager(Handler handler) {
+ public KeyCombinationManager(Handler handler) {
mHandler = handler;
}
- void addRule(TwoKeysCombinationRule rule) {
+ public void addRule(TwoKeysCombinationRule rule) {
if (mRules.contains(rule)) {
throw new IllegalArgumentException("Rule : " + rule + " already exists.");
}
mRules.add(rule);
}
- void removeRule(TwoKeysCombinationRule rule) {
+ public void removeRule(TwoKeysCombinationRule rule) {
mRules.remove(rule);
}
@@ -148,7 +148,7 @@
* to a window.
* Return true if any active rule could be triggered by the key event, otherwise false.
*/
- boolean interceptKey(KeyEvent event, boolean interactive) {
+ public boolean interceptKey(KeyEvent event, boolean interactive) {
synchronized (mLock) {
return interceptKeyLocked(event, interactive);
}
@@ -226,7 +226,7 @@
/**
* Return the interceptTimeout to tell InputDispatcher when is ready to deliver to window.
*/
- long getKeyInterceptTimeout(int keyCode) {
+ public long getKeyInterceptTimeout(int keyCode) {
synchronized (mLock) {
if (mDownTimes.get(keyCode) == 0) {
return 0;
@@ -246,7 +246,7 @@
/**
* True if the key event had been handled.
*/
- boolean isKeyConsumed(KeyEvent event) {
+ public boolean isKeyConsumed(KeyEvent event) {
synchronized (mLock) {
if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) != 0) {
return false;
@@ -258,7 +258,7 @@
/**
* True if power key is the candidate.
*/
- boolean isPowerKeyIntercepted() {
+ public boolean isPowerKeyIntercepted() {
synchronized (mLock) {
if (forAllActiveRules((rule) -> rule.shouldInterceptKey(KEYCODE_POWER))) {
// return false if only if power key pressed.
@@ -294,7 +294,7 @@
return false;
}
- void dump(String prefix, PrintWriter pw) {
+ public void dump(String prefix, PrintWriter pw) {
pw.println(prefix + "KeyCombination rules:");
forAllRules(mRules, (rule)-> {
pw.println(prefix + " " + rule);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index ca60518..2284050 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -86,6 +86,7 @@
import static com.android.hardware.input.Flags.emojiAndScreenshotKeycodesAvailable;
import static com.android.hardware.input.Flags.modifierShortcutDump;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
+import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures;
import static com.android.server.flags.Flags.modifierShortcutManagerMultiuser;
import static com.android.server.flags.Flags.newBugreportKeyboardShortcut;
import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY;
@@ -198,7 +199,6 @@
import android.view.IDisplayFoldListener;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
-import android.view.KeyCharacterMap.FallbackAction;
import android.view.KeyEvent;
import android.view.KeyboardShortcutGroup;
import android.view.MotionEvent;
@@ -698,10 +698,6 @@
// Maps global key codes to the components that will handle them.
private GlobalKeyManager mGlobalKeyManager;
- // Fallback actions by key code.
- private final SparseArray<KeyCharacterMap.FallbackAction> mFallbackActions =
- new SparseArray<KeyCharacterMap.FallbackAction>();
-
private final com.android.internal.policy.LogDecelerateInterpolator mLogDecelerateInterpolator
= new LogDecelerateInterpolator(100, 0);
private final DeferredKeyActionExecutor mDeferredKeyActionExecutor =
@@ -1056,7 +1052,8 @@
return handled;
}
- private void interceptPowerKeyDown(KeyEvent event, boolean interactive) {
+ private void interceptPowerKeyDown(KeyEvent event, boolean interactive,
+ boolean isKeyGestureTriggered) {
// Hold a wake lock until the power key is released.
if (!mPowerKeyWakeLock.isHeld()) {
mPowerKeyWakeLock.acquire();
@@ -1089,7 +1086,8 @@
// If the power key has still not yet been handled, then detect short
// press, long press, or multi press and decide what to do.
mPowerKeyHandled = mPowerKeyHandled || hungUp
- || handledByPowerManager || mKeyCombinationManager.isPowerKeyIntercepted();
+ || handledByPowerManager || isKeyGestureTriggered
+ || mKeyCombinationManager.isPowerKeyIntercepted();
if (!mPowerKeyHandled) {
if (!interactive) {
wakeUpFromWakeKey(event);
@@ -2465,6 +2463,9 @@
private void initKeyCombinationRules() {
mKeyCombinationManager = new KeyCombinationManager(mHandler);
+ if (useKeyGestureEventHandler() && useKeyGestureEventHandlerMultiPressGestures()) {
+ return;
+ }
final boolean screenshotChordEnabled = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_enableScreenshotChord);
@@ -2472,13 +2473,13 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_POWER) {
@Override
- void execute() {
+ public void execute() {
mPowerKeyHandled = true;
interceptScreenshotChord(
SCREENSHOT_KEY_CHORD, getScreenshotChordLongPressDelay());
}
@Override
- void cancel() {
+ public void cancel() {
cancelPendingScreenshotChordAction();
}
});
@@ -2487,13 +2488,13 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_POWER, KEYCODE_STEM_PRIMARY) {
@Override
- void execute() {
+ public void execute() {
mPowerKeyHandled = true;
interceptScreenshotChord(SCREENSHOT_KEY_CHORD,
getScreenshotChordLongPressDelay());
}
@Override
- void cancel() {
+ public void cancel() {
cancelPendingScreenshotChordAction();
}
});
@@ -2503,16 +2504,16 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP) {
@Override
- boolean preCondition() {
+ public boolean preCondition() {
return mAccessibilityShortcutController
.isAccessibilityShortcutAvailable(isKeyguardLocked());
}
@Override
- void execute() {
+ public void execute() {
interceptAccessibilityShortcutChord();
}
@Override
- void cancel() {
+ public void cancel() {
cancelPendingAccessibilityShortcutAction();
}
});
@@ -2523,7 +2524,7 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_VOLUME_UP, KEYCODE_POWER) {
@Override
- boolean preCondition() {
+ public boolean preCondition() {
switch (mPowerVolUpBehavior) {
case POWER_VOLUME_UP_BEHAVIOR_MUTE:
return mRingerToggleChord != VOLUME_HUSH_OFF;
@@ -2532,7 +2533,7 @@
}
}
@Override
- void execute() {
+ public void execute() {
switch (mPowerVolUpBehavior) {
case POWER_VOLUME_UP_BEHAVIOR_MUTE:
// no haptic feedback here since
@@ -2551,7 +2552,7 @@
}
}
@Override
- void cancel() {
+ public void cancel() {
switch (mPowerVolUpBehavior) {
case POWER_VOLUME_UP_BEHAVIOR_MUTE:
cancelPendingRingerToggleChordAction();
@@ -2567,16 +2568,16 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_BACK, KEYCODE_DPAD_DOWN) {
@Override
- void execute() {
+ public void execute() {
mBackKeyHandled = true;
interceptAccessibilityGestureTv();
}
@Override
- void cancel() {
+ public void cancel() {
cancelAccessibilityGestureTv();
}
@Override
- long getKeyInterceptDelayMs() {
+ public long getKeyInterceptDelayMs() {
// Use a timeout of 0 to prevent additional latency in processing of
// this key. This will potentially cause some unwanted UI actions if the
// user does end up triggering the key combination later, but in most
@@ -2590,16 +2591,16 @@
mKeyCombinationManager.addRule(
new TwoKeysCombinationRule(KEYCODE_DPAD_CENTER, KEYCODE_BACK) {
@Override
- void execute() {
+ public void execute() {
mBackKeyHandled = true;
interceptBugreportGestureTv();
}
@Override
- void cancel() {
+ public void cancel() {
cancelBugreportGestureTv();
}
@Override
- long getKeyInterceptDelayMs() {
+ public long getKeyInterceptDelayMs() {
return 0;
}
});
@@ -3403,15 +3404,18 @@
+ keyguardOn() + " canceled=" + event.isCanceled());
}
- if (mKeyCombinationManager.isKeyConsumed(event)) {
- return keyConsumed;
- }
+ if (!useKeyGestureEventHandler()) {
+ if (mKeyCombinationManager.isKeyConsumed(event)) {
+ return keyConsumed;
+ }
- if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
- final long now = SystemClock.uptimeMillis();
- final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(keyCode);
- if (now < interceptTimeout) {
- return interceptTimeout - now;
+ if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
+ final long now = SystemClock.uptimeMillis();
+ final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(
+ keyCode);
+ if (now < interceptTimeout) {
+ return interceptTimeout - now;
+ }
}
}
@@ -3453,8 +3457,16 @@
// NOTE: Please try not to add new Shortcut combinations here and instead use KeyGestureEvents.
// Add shortcut trigger logic in {@code KeyGestureController} and add handling logic in
// {@link handleKeyGesture()}
- @SuppressLint("MissingPermission")
private boolean interceptSystemKeysAndShortcuts(IBinder focusedToken, KeyEvent event) {
+ if (useKeyGestureEventHandler()) {
+ return interceptSystemKeysAndShortcutsNew(focusedToken, event);
+ } else {
+ return interceptSystemKeysAndShortcutsOld(focusedToken, event);
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private boolean interceptSystemKeysAndShortcutsOld(IBinder focusedToken, KeyEvent event) {
final boolean keyguardOn = keyguardOn();
final int keyCode = event.getKeyCode();
final int repeatCount = event.getRepeatCount();
@@ -3878,6 +3890,59 @@
return (metaState & KeyEvent.META_META_ON) != 0;
}
+ private boolean interceptSystemKeysAndShortcutsNew(IBinder focusedToken, KeyEvent event) {
+ final int keyCode = event.getKeyCode();
+ final int metaState = event.getMetaState();
+ final boolean keyguardOn = keyguardOn();
+
+ if (isUserSetupComplete() && !keyguardOn) {
+ if (mModifierShortcutManager.interceptKey(event)) {
+ dismissKeyboardShortcutsMenu();
+ return true;
+ }
+ }
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_HOME:
+ return handleHomeShortcuts(focusedToken, event);
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ if (mUseTvRouting || mHandleVolumeKeysInWM) {
+ // On TVs or when the configuration is enabled, volume keys never
+ // go to the foreground app.
+ dispatchDirectAudioEvent(event);
+ return true;
+ }
+
+ // If the device is in VR mode and keys are "internal" (e.g. on the side of the
+ // device), then drop the volume keys and don't forward it to the
+ // application/dispatch the audio event.
+ if (mDefaultDisplayPolicy.isPersistentVrModeEnabled()) {
+ final InputDevice d = event.getDevice();
+ if (d != null && !d.isExternal()) {
+ return true;
+ }
+ }
+ break;
+ case KeyEvent.KEYCODE_STEM_PRIMARY:
+ if (prepareToSendSystemKeyToApplication(focusedToken, event)) {
+ // Send to app.
+ return false;
+ } else {
+ // Intercepted.
+ sendSystemKeyToStatusBarAsync(event);
+ return true;
+ }
+ }
+ if (isValidGlobalKey(keyCode)
+ && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) {
+ return true;
+ }
+
+ // Reserve all the META modifier combos for system behavior
+ return (metaState & KeyEvent.META_META_ON) != 0;
+ }
+
@SuppressLint("MissingPermission")
private void initKeyGestures() {
if (!useKeyGestureEventHandler()) {
@@ -3887,7 +3952,13 @@
@Override
public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
@Nullable IBinder focusedToken) {
- return PhoneWindowManager.this.handleKeyGestureEvent(event, focusedToken);
+ boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
+ focusedToken);
+ if (handled && Arrays.stream(event.getKeycodes()).anyMatch(
+ (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
+ mPowerKeyHandled = true;
+ }
+ return handled;
}
@Override
@@ -3917,7 +3988,20 @@
case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH:
case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
+ case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
+ case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
+ case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
+ case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
+ return mDefaultDisplayPolicy.isAwake();
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+ return mDefaultDisplayPolicy.isAwake() && mAccessibilityShortcutController
+ .isAccessibilityShortcutAvailable(isKeyguardLocked());
+ case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+ return mDefaultDisplayPolicy.isAwake() && mAccessibilityShortcutController
+ .isAccessibilityShortcutAvailable(false);
default:
return false;
}
@@ -4075,6 +4159,66 @@
sendSwitchKeyboardLayout(displayId, focusedToken, direction);
}
return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
+ if (start) {
+ // Screenshot chord is pressed: Wait for long press delay before taking
+ // screenshot
+ interceptScreenshotChord(SCREENSHOT_KEY_CHORD,
+ getScreenshotChordLongPressDelay());
+ } else {
+ cancelPendingScreenshotChordAction();
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+ if (start) {
+ interceptAccessibilityShortcutChord();
+ } else {
+ cancelPendingAccessibilityShortcutAction();
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
+ if (start) {
+ interceptRingerToggleChord();
+ } else {
+ cancelPendingRingerToggleChordAction();
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
+ if (start) {
+ performHapticFeedback(
+ HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON,
+ "KEY_GESTURE_TYPE_GLOBAL_ACTIONS - Global Actions");
+ showGlobalActions();
+ } else {
+ cancelGlobalActionsAction();
+ }
+ return true;
+ // TODO (b/358569822): Consolidate TV and non-TV gestures into same KeyGestureEvent
+ case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
+ if (start) {
+ interceptAccessibilityGestureTv();
+ } else {
+ cancelAccessibilityGestureTv();
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
+ if (start) {
+ interceptBugreportGestureTv();
+ } else {
+ cancelBugreportGestureTv();
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
+ if (complete && mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+ isKeyguardLocked())) {
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT));
+ }
+ return true;
+ case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
+ if (complete) {
+ mContext.closeSystemDialogs();
+ }
+ return true;
}
return false;
}
@@ -4246,7 +4390,8 @@
mHandler.removeMessages(MSG_ACCESSIBILITY_TV);
}
- private void requestBugreportForTv() {
+ @VisibleForTesting
+ void requestBugreportForTv() {
try {
if (!ActivityManager.getService().launchBugReportHandlerApp()) {
ActivityManager.getService().requestInteractiveBugReport();
@@ -4259,7 +4404,7 @@
// TODO(b/117479243): handle it in InputPolicy
/** {@inheritDoc} */
@Override
- public KeyEvent dispatchUnhandledKey(IBinder focusedToken, KeyEvent event, int policyFlags) {
+ public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
// Note: This method is only called if the initial down was unhandled.
if (DEBUG_INPUT) {
final KeyInterceptionInfo info =
@@ -4272,75 +4417,26 @@
+ ", keyCode=" + event.getKeyCode()
+ ", scanCode=" + event.getScanCode()
+ ", metaState=" + event.getMetaState()
- + ", repeatCount=" + event.getRepeatCount()
- + ", policyFlags=" + policyFlags);
+ + ", repeatCount=" + event.getRepeatCount());
}
- if (interceptUnhandledKey(event, focusedToken)) {
- return null;
- }
-
- KeyEvent fallbackEvent = null;
- if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
- final KeyCharacterMap kcm = event.getKeyCharacterMap();
- final int keyCode = event.getKeyCode();
- final int metaState = event.getMetaState();
- final boolean initialDown = event.getAction() == KeyEvent.ACTION_DOWN
- && event.getRepeatCount() == 0;
-
- // Check for fallback actions specified by the key character map.
- final FallbackAction fallbackAction;
- if (initialDown) {
- fallbackAction = kcm.getFallbackAction(keyCode, metaState);
- } else {
- fallbackAction = mFallbackActions.get(keyCode);
- }
-
- if (fallbackAction != null) {
- if (DEBUG_INPUT) {
- Slog.d(TAG, "Fallback: keyCode=" + fallbackAction.keyCode
- + " metaState=" + Integer.toHexString(fallbackAction.metaState));
- }
-
- final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK;
- fallbackEvent = KeyEvent.obtain(
- event.getDownTime(), event.getEventTime(),
- event.getAction(), fallbackAction.keyCode,
- event.getRepeatCount(), fallbackAction.metaState,
- event.getDeviceId(), event.getScanCode(),
- flags, event.getSource(), event.getDisplayId(), null);
-
- if (!interceptFallback(focusedToken, fallbackEvent, policyFlags)) {
- fallbackEvent.recycle();
- fallbackEvent = null;
- }
-
- if (initialDown) {
- mFallbackActions.put(keyCode, fallbackAction);
- } else if (event.getAction() == KeyEvent.ACTION_UP) {
- mFallbackActions.remove(keyCode);
- fallbackAction.recycle();
- }
- }
- }
-
- if (DEBUG_INPUT) {
- if (fallbackEvent == null) {
- Slog.d(TAG, "No fallback.");
- } else {
- Slog.d(TAG, "Performing fallback: " + fallbackEvent);
- }
- }
- return fallbackEvent;
- }
-
- private boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
final int keyCode = event.getKeyCode();
final int repeatCount = event.getRepeatCount();
final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
final int metaState = event.getModifiers();
- switch(keyCode) {
+ // TODO(b/358569822): Shift to KeyGestureEvent based handling
+ if (keyCode == KeyEvent.KEYCODE_STEM_PRIMARY) {
+ handleUnhandledSystemKey(event);
+ sendSystemKeyToStatusBarAsync(event);
+ return true;
+ }
+
+ if (useKeyGestureEventHandler()) {
+ return false;
+ }
+
+ switch (keyCode) {
case KeyEvent.KEYCODE_SPACE:
if (down && repeatCount == 0) {
// Handle keyboard layout switching. (CTRL + SPACE)
@@ -4377,10 +4473,6 @@
return true;
}
break;
- case KeyEvent.KEYCODE_STEM_PRIMARY:
- handleUnhandledSystemKey(event);
- sendSystemKeyToStatusBarAsync(event);
- return true;
}
return false;
@@ -4419,19 +4511,6 @@
targetWindowToken);
}
- private boolean interceptFallback(IBinder focusedToken, KeyEvent fallbackEvent,
- int policyFlags) {
- int actions = interceptKeyBeforeQueueing(fallbackEvent, policyFlags);
- if ((actions & ACTION_PASS_TO_USER) != 0) {
- long delayMillis = interceptKeyBeforeDispatching(
- focusedToken, fallbackEvent, policyFlags);
- if (delayMillis == 0 && !interceptUnhandledKey(fallbackEvent, focusedToken)) {
- return true;
- }
- }
- return false;
- }
-
@Override
public void setTopFocusedDisplay(int displayId) {
mTopFocusedDisplayId = displayId;
@@ -4888,6 +4967,7 @@
final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
boolean isWakeKey = (policyFlags & WindowManagerPolicy.FLAG_WAKE) != 0
|| event.isWakeKey();
+ boolean isKeyGestureTriggered = (policyFlags & FLAG_KEY_GESTURE_TRIGGERED) != 0;
// There are key events that perform the operation as the current user,
// and these should be ignored for visible background users.
@@ -5014,8 +5094,13 @@
final boolean isDefaultDisplayOn = Display.isOnState(mDefaultDisplay.getState());
final boolean isDefaultDisplayAwake = mDefaultDisplayPolicy.isAwake();
final boolean interactiveAndAwake = interactive && isDefaultDisplayAwake;
- if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
- handleKeyGesture(event, interactiveAndAwake, isDefaultDisplayOn);
+ if (isKeyGestureTriggered) {
+ // If key gesture is triggered outside policy, reset gesture handlers here
+ mSingleKeyGestureDetector.reset();
+ } else {
+ if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
+ handleKeyGesture(event, interactiveAndAwake, isDefaultDisplayOn);
+ }
}
// Enable haptics if down and virtual key without multiple repetitions. If this is a hard
@@ -5178,7 +5263,7 @@
result &= ~ACTION_PASS_TO_USER;
isWakeKey = false; // wake-up will be handled separately
if (down) {
- interceptPowerKeyDown(event, interactiveAndAwake);
+ interceptPowerKeyDown(event, interactiveAndAwake, isKeyGestureTriggered);
} else {
interceptPowerKeyUp(event, canceled);
}
diff --git a/services/core/java/com/android/server/policy/WindowManagerPolicy.java b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
index 892af6b..ad11657 100644
--- a/services/core/java/com/android/server/policy/WindowManagerPolicy.java
+++ b/services/core/java/com/android/server/policy/WindowManagerPolicy.java
@@ -754,11 +754,9 @@
* @param focusedToken Client window token that currently has focus. This is where the key
* event will normally go.
* @param event The key event.
- * @param policyFlags The policy flags associated with the key.
- * @return Returns an alternate key event to redispatch as a fallback, or null to give up.
- * The caller is responsible for recycling the key event.
+ * @return true if the unhandled key is intercepted by the policy.
*/
- KeyEvent dispatchUnhandledKey(IBinder focusedToken, KeyEvent event, int policyFlags);
+ boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken);
/**
* Called when the top focused display is changed.
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 303828f..0cdf537 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -53,6 +53,7 @@
import android.util.EventLog;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import android.view.WindowManagerPolicyConstants;
import com.android.internal.annotations.VisibleForTesting;
@@ -512,8 +513,17 @@
}
// Start input as soon as we start waking up or going to sleep.
- mInputManagerInternal.setInteractive(interactive);
mInputMethodManagerInternal.setInteractive(interactive);
+ if (!mFlags.isPerDisplayWakeByTouchEnabled()) {
+ // Since wakefulness is a global property in original logic, all displays should
+ // be set to the same interactive state, matching system's global wakefulness
+ SparseBooleanArray displayInteractivities = new SparseBooleanArray();
+ int[] displayIds = mDisplayManagerInternal.getDisplayIds().toArray();
+ for (int displayId : displayIds) {
+ displayInteractivities.put(displayId, interactive);
+ }
+ mInputManagerInternal.setDisplayInteractivities(displayInteractivities);
+ }
// Notify battery stats.
try {
diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
index c6ef89d..fd60e06 100644
--- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
+++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
@@ -42,6 +42,11 @@
Flags::improveWakelockLatency
);
+ private final FlagState mPerDisplayWakeByTouch = new FlagState(
+ Flags.FLAG_PER_DISPLAY_WAKE_BY_TOUCH,
+ Flags::perDisplayWakeByTouch
+ );
+
/** Returns whether early-screen-timeout-detector is enabled on not. */
public boolean isEarlyScreenTimeoutDetectorEnabled() {
return mEarlyScreenTimeoutDetectorFlagState.isEnabled();
@@ -55,6 +60,13 @@
}
/**
+ * @return Whether per-display wake by touch is enabled or not.
+ */
+ public boolean isPerDisplayWakeByTouchEnabled() {
+ return mPerDisplayWakeByTouch.isEnabled();
+ }
+
+ /**
* dumps all flagstates
* @param pw printWriter
*/
@@ -62,6 +74,7 @@
pw.println("PowerManagerFlags:");
pw.println(" " + mEarlyScreenTimeoutDetectorFlagState);
pw.println(" " + mImproveWakelockLatency);
+ pw.println(" " + mPerDisplayWakeByTouch);
}
private static class FlagState {
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
index 3581b2f..9cf3bb6 100644
--- a/services/core/java/com/android/server/power/feature/power_flags.aconfig
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -17,4 +17,12 @@
description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock."
bug: "339590565"
is_fixed_read_only: true
-}
\ No newline at end of file
+}
+
+flag {
+ name: "per_display_wake_by_touch"
+ namespace: "power"
+ description: "Feature flag to enable per-display wake by touch"
+ bug: "343295183"
+ is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java b/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java
new file mode 100644
index 0000000..dd6d5db
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/AccumulatedBatteryUsageStatsSection.java
@@ -0,0 +1,67 @@
+/*
+ * 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.power.stats;
+
+import android.os.BatteryUsageStats;
+import android.util.IndentingPrintWriter;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+class AccumulatedBatteryUsageStatsSection extends PowerStatsSpan.Section {
+ public static final String TYPE = "accumulated-battery-usage-stats";
+ public static final long ID = Long.MAX_VALUE;
+
+ private final BatteryUsageStats.Builder mBatteryUsageStats;
+
+ AccumulatedBatteryUsageStatsSection(BatteryUsageStats.Builder batteryUsageStats) {
+ super(TYPE);
+ mBatteryUsageStats = batteryUsageStats;
+ }
+
+ public BatteryUsageStats.Builder getBatteryUsageStatsBuilder() {
+ return mBatteryUsageStats;
+ }
+
+ @Override
+ public void write(TypedXmlSerializer serializer) throws IOException {
+ mBatteryUsageStats.build().writeXml(serializer);
+ }
+
+ @Override
+ public void dump(IndentingPrintWriter ipw) {
+ mBatteryUsageStats.build().dump(ipw, "");
+ }
+
+ static class Reader implements PowerStatsSpan.SectionReader {
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+
+ @Override
+ public PowerStatsSpan.Section read(String sectionType, TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ return new AccumulatedBatteryUsageStatsSection(
+ BatteryUsageStats.createBuilderFromXml(parser));
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index cb8e1a0..3f1d9a3 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -509,6 +509,7 @@
}
private boolean mSaveBatteryUsageStatsOnReset;
+ private boolean mAccumulateBatteryUsageStats;
private BatteryUsageStatsProvider mBatteryUsageStatsProvider;
private PowerStatsStore mPowerStatsStore;
@@ -11975,10 +11976,12 @@
*/
public void saveBatteryUsageStatsOnReset(
@NonNull BatteryUsageStatsProvider batteryUsageStatsProvider,
- @NonNull PowerStatsStore powerStatsStore) {
+ @NonNull PowerStatsStore powerStatsStore,
+ boolean accumulateBatteryUsageStats) {
mSaveBatteryUsageStatsOnReset = true;
mBatteryUsageStatsProvider = batteryUsageStatsProvider;
mPowerStatsStore = powerStatsStore;
+ mAccumulateBatteryUsageStats = accumulateBatteryUsageStats;
}
@GuardedBy("this")
@@ -12179,29 +12182,33 @@
return;
}
- final BatteryUsageStats batteryUsageStats;
- synchronized (this) {
- batteryUsageStats = mBatteryUsageStatsProvider.getBatteryUsageStats(this,
- new BatteryUsageStatsQuery.Builder()
- .setMaxStatsAgeMs(0)
- .includePowerModels()
- .includeProcessStateData()
- .build());
- }
-
- // TODO(b/188068523): BatteryUsageStats should use monotonic time for start and end
- // Once that change is made, we will be able to use the BatteryUsageStats' monotonic
- // start time
- long monotonicStartTime =
- mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
- mHandler.post(() -> {
- mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats);
- try {
- batteryUsageStats.close();
- } catch (IOException e) {
- Log.e(TAG, "Cannot close BatteryUsageStats", e);
+ if (mAccumulateBatteryUsageStats) {
+ mBatteryUsageStatsProvider.accumulateBatteryUsageStats(this);
+ } else {
+ final BatteryUsageStats batteryUsageStats;
+ synchronized (this) {
+ batteryUsageStats = mBatteryUsageStatsProvider.getBatteryUsageStats(this,
+ new BatteryUsageStatsQuery.Builder()
+ .setMaxStatsAgeMs(0)
+ .includePowerModels()
+ .includeProcessStateData()
+ .build());
}
- });
+
+ // TODO(b/188068523): BatteryUsageStats should use monotonic time for start and end
+ // Once that change is made, we will be able to use the BatteryUsageStats' monotonic
+ // start time
+ long monotonicStartTime =
+ mMonotonicClock.monotonicTime() - batteryUsageStats.getStatsDuration();
+ mHandler.post(() -> {
+ mPowerStatsStore.storeBatteryUsageStats(monotonicStartTime, batteryUsageStats);
+ try {
+ batteryUsageStats.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Cannot close BatteryUsageStats", e);
+ }
+ });
+ }
}
@GuardedBy("this")
diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
index 87a3e5e..d66e05b 100644
--- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
+++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java
@@ -33,6 +33,7 @@
import com.android.internal.os.CpuScalingPolicies;
import com.android.internal.os.PowerProfile;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -64,6 +65,7 @@
mClock = clock;
mPowerStatsStore.addSectionReader(new BatteryUsageStatsSection.Reader());
+ mPowerStatsStore.addSectionReader(new AccumulatedBatteryUsageStatsSection.Reader());
}
private List<PowerCalculator> getPowerCalculators() {
@@ -151,6 +153,56 @@
}
/**
+ * Compute BatteryUsageStats for the period since the last accumulated stats were stored,
+ * add them to the accumulated stats and save the result.
+ */
+ public void accumulateBatteryUsageStats(BatteryStatsImpl stats) {
+ BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
+
+ PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
+ AccumulatedBatteryUsageStatsSection.ID,
+ AccumulatedBatteryUsageStatsSection.TYPE);
+ if (powerStatsSpan != null) {
+ List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
+ for (int i = sections.size() - 1; i >= 0; i--) {
+ PowerStatsSpan.Section section = sections.get(i);
+ if (AccumulatedBatteryUsageStatsSection.TYPE.equals(section.getType())) {
+ accumulatedBatteryUsageStatsBuilder =
+ ((AccumulatedBatteryUsageStatsSection) section)
+ .getBatteryUsageStatsBuilder();
+ break;
+ }
+ }
+ }
+
+ // TODO(b/366493365): add the current batteryusagestats directly into the "accumulated"
+ // builder to avoid allocating a second CursorWindow
+ BatteryUsageStats.Builder currentBatteryUsageStatsBuilder =
+ getCurrentBatteryUsageStatsBuilder(stats,
+ new BatteryUsageStatsQuery.Builder()
+ .setMaxStatsAgeMs(0)
+ .includeProcessStateData()
+ .includePowerStateData()
+ .includeScreenStateData()
+ .build(),
+ mClock.currentTimeMillis());
+
+ if (accumulatedBatteryUsageStatsBuilder == null) {
+ accumulatedBatteryUsageStatsBuilder = currentBatteryUsageStatsBuilder;
+ } else {
+ accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStatsBuilder.build());
+ currentBatteryUsageStatsBuilder.discard();
+ }
+
+ powerStatsSpan = new PowerStatsSpan(AccumulatedBatteryUsageStatsSection.ID);
+ powerStatsSpan.addSection(
+ new AccumulatedBatteryUsageStatsSection(accumulatedBatteryUsageStatsBuilder));
+
+ mPowerStatsStore.storePowerStatsSpanAsync(powerStatsSpan,
+ accumulatedBatteryUsageStatsBuilder::discard);
+ }
+
+ /**
* Returns true if the last update was too long ago for the tolerances specified
* by the supplied queries.
*/
@@ -192,15 +244,67 @@
private BatteryUsageStats getBatteryUsageStats(BatteryStatsImpl stats,
BatteryUsageStatsQuery query, long currentTimeMs) {
- if (query.getToTimestamp() == 0) {
+ if ((query.getFlags()
+ & BatteryUsageStatsQuery.FLAG_BATTERY_USAGE_STATS_ACCUMULATED) != 0) {
+ return getAccumulatedBatteryUsageStats(stats, query);
+ } else if (query.getToTimestamp() == 0) {
return getCurrentBatteryUsageStats(stats, query, currentTimeMs);
} else {
return getAggregatedBatteryUsageStats(stats, query);
}
}
+ private BatteryUsageStats getAccumulatedBatteryUsageStats(BatteryStatsImpl stats,
+ BatteryUsageStatsQuery query) {
+ PowerStatsSpan powerStatsSpan = mPowerStatsStore.loadPowerStatsSpan(
+ AccumulatedBatteryUsageStatsSection.ID,
+ AccumulatedBatteryUsageStatsSection.TYPE);
+
+ BatteryUsageStats.Builder accumulatedBatteryUsageStatsBuilder = null;
+ if (powerStatsSpan != null) {
+ List<PowerStatsSpan.Section> sections = powerStatsSpan.getSections();
+ if (sections.size() == 1) {
+ accumulatedBatteryUsageStatsBuilder =
+ ((AccumulatedBatteryUsageStatsSection) sections.get(0))
+ .getBatteryUsageStatsBuilder();
+ } else {
+ Slog.wtf(TAG, "Unexpected number of sections for type "
+ + AccumulatedBatteryUsageStatsSection.TYPE);
+ }
+ }
+
+ BatteryUsageStats currentBatteryUsageStats = getCurrentBatteryUsageStats(stats, query,
+ mClock.currentTimeMillis());
+
+ BatteryUsageStats result;
+ if (accumulatedBatteryUsageStatsBuilder == null) {
+ result = currentBatteryUsageStats;
+ } else {
+ accumulatedBatteryUsageStatsBuilder.add(currentBatteryUsageStats);
+ try {
+ currentBatteryUsageStats.close();
+ } catch (IOException ex) {
+ Slog.e(TAG, "Closing BatteryUsageStats", ex);
+ }
+ result = accumulatedBatteryUsageStatsBuilder.build();
+ }
+
+ return result;
+ }
+
private BatteryUsageStats getCurrentBatteryUsageStats(BatteryStatsImpl stats,
BatteryUsageStatsQuery query, long currentTimeMs) {
+ BatteryUsageStats.Builder builder = getCurrentBatteryUsageStatsBuilder(stats, query,
+ currentTimeMs);
+ BatteryUsageStats batteryUsageStats = builder.build();
+ if (batteryUsageStats.isProcessStateDataIncluded()) {
+ verify(batteryUsageStats);
+ }
+ return batteryUsageStats;
+ }
+
+ private BatteryUsageStats.Builder getCurrentBatteryUsageStatsBuilder(BatteryStatsImpl stats,
+ BatteryUsageStatsQuery query, long currentTimeMs) {
final long realtimeUs = mClock.elapsedRealtime() * 1000;
final long uptimeUs = mClock.uptimeMillis() * 1000;
@@ -274,11 +378,7 @@
mPowerAttributor.estimatePowerConsumption(batteryUsageStatsBuilder, stats.getHistory(),
monotonicStartTime, monotonicEndTime);
- BatteryUsageStats batteryUsageStats = batteryUsageStatsBuilder.build();
- if (includeProcessStateData) {
- verify(batteryUsageStats);
- }
- return batteryUsageStats;
+ return batteryUsageStatsBuilder;
}
// STOPSHIP(b/229906525): remove verification before shipping
diff --git a/services/core/java/com/android/server/power/stats/PowerStatsStore.java b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
index a875c30..5a6f973 100644
--- a/services/core/java/com/android/server/power/stats/PowerStatsStore.java
+++ b/services/core/java/com/android/server/power/stats/PowerStatsStore.java
@@ -133,6 +133,19 @@
}
/**
+ * Schedules saving the specified span on the background thread.
+ */
+ public void storePowerStatsSpanAsync(PowerStatsSpan span, Runnable onComplete) {
+ mHandler.post(() -> {
+ try {
+ storePowerStatsSpan(span);
+ } finally {
+ onComplete.run();
+ }
+ });
+ }
+
+ /**
* Saves the specified span in the store.
*/
public void storePowerStatsSpan(PowerStatsSpan span) {
@@ -172,6 +185,9 @@
lockStoreDirectory();
try {
File file = makePowerStatsSpanFilename(id);
+ if (!file.exists()) {
+ return null;
+ }
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
return PowerStatsSpan.read(inputStream, parser, mSectionReaders, sectionTypes);
} catch (IOException | XmlPullParserException e) {
diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig
index 05d29f5..ce6f57f 100644
--- a/services/core/java/com/android/server/power/stats/flags.aconfig
+++ b/services/core/java/com/android/server/power/stats/flags.aconfig
@@ -76,3 +76,13 @@
bug: "364350206"
is_fixed_read_only: true
}
+
+flag {
+ name: "accumulate_battery_usage_stats"
+ namespace: "backstage_power"
+ description: "Add support for monotonically accumulated BatteryUsageStats"
+ bug: "345022340"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
index f047f56..ab630ee 100644
--- a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
+++ b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java
@@ -17,6 +17,7 @@
package com.android.server.power.stats.wakeups;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
@@ -63,6 +64,7 @@
private static final String SUBSYSTEM_SOUND_TRIGGER_STRING = "Sound_trigger";
private static final String SUBSYSTEM_SENSOR_STRING = "Sensor";
private static final String SUBSYSTEM_CELLULAR_DATA_STRING = "Cellular_data";
+ private static final String SUBSYSTEM_BLUETOOTH_STRING = "Bluetooth";
private static final String TRACE_TRACK_WAKEUP_ATTRIBUTION = "wakeup_attribution";
private static final long WAKEUP_WRITE_DELAY_MS = TimeUnit.SECONDS.toMillis(30);
@@ -512,6 +514,8 @@
return CPU_WAKEUP_SUBSYSTEM_SENSOR;
case SUBSYSTEM_CELLULAR_DATA_STRING:
return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
+ case SUBSYSTEM_BLUETOOTH_STRING:
+ return CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
}
return CPU_WAKEUP_SUBSYSTEM_UNKNOWN;
}
@@ -528,6 +532,8 @@
return SUBSYSTEM_SENSOR_STRING;
case CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA:
return SUBSYSTEM_CELLULAR_DATA_STRING;
+ case CPU_WAKEUP_SUBSYSTEM_BLUETOOTH:
+ return SUBSYSTEM_BLUETOOTH_STRING;
case CPU_WAKEUP_SUBSYSTEM_UNKNOWN:
return "Unknown";
}
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index b35a0a7..74c1124e 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -216,13 +216,13 @@
import com.android.server.BinderCallsStatsService;
import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
-import com.android.server.PinnerService;
-import com.android.server.PinnerService.PinnedFileStats;
import com.android.server.SystemService;
import com.android.server.SystemServiceManager;
import com.android.server.am.MemoryStatUtil.MemoryStat;
import com.android.server.health.HealthServiceWrapper;
import com.android.server.notification.NotificationManagerService;
+import com.android.server.pinner.PinnerService;
+import com.android.server.pinner.PinnerService.PinnedFileStats;
import com.android.server.pm.UserManagerInternal;
import com.android.server.power.stats.KernelWakelockReader;
import com.android.server.power.stats.KernelWakelockStats;
diff --git a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
index b4d3862..b5a7fcb 100644
--- a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.ExternalVibration;
@@ -24,6 +25,7 @@
import android.os.VibrationAttributes;
import android.os.vibrator.Flags;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.FrameworkStatsLog;
/**
@@ -32,14 +34,16 @@
final class ExternalVibrationSession extends Vibration
implements VibrationSession, IBinder.DeathRecipient {
+ private final Object mLock = new Object();
private final ExternalVibration mExternalVibration;
private final ExternalVibrationScale mScale = new ExternalVibrationScale();
+ @GuardedBy("mLock")
@Nullable
private Runnable mBinderDeathCallback;
ExternalVibrationSession(ExternalVibration externalVibration) {
- super(externalVibration.getToken(), new CallerInfo(
+ super(new CallerInfo(
externalVibration.getVibrationAttributes(), externalVibration.getUid(),
// TODO(b/249785241): Find a way to link ExternalVibration to a VirtualDevice
// instead of using DEVICE_ID_INVALID here and relying on the UID checks.
@@ -82,24 +86,25 @@
}
@Override
- public void linkToDeath(Runnable callback) {
- synchronized (this) {
+ public boolean linkToDeath(Runnable callback) {
+ synchronized (mLock) {
mBinderDeathCallback = callback;
}
mExternalVibration.linkToDeath(this);
+ return true;
}
@Override
public void unlinkToDeath() {
mExternalVibration.unlinkToDeath(this);
- synchronized (this) {
+ synchronized (mLock) {
mBinderDeathCallback = null;
}
}
@Override
public void binderDied() {
- synchronized (this) {
+ synchronized (mLock) {
if (mBinderDeathCallback != null) {
mBinderDeathCallback.run();
}
@@ -119,9 +124,11 @@
}
@Override
- public void notifyEnded() {
+ public void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy,
+ boolean immediate) {
// Notify external client that this vibration should stop sending data to the vibrator.
mExternalVibration.mute();
+ end(new EndInfo(status, endedBy));
}
boolean isHoldingSameVibration(ExternalVibration vib) {
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index ea4bd01..ce9c47b 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -36,6 +36,7 @@
final class HalVibration extends Vibration {
public final SparseArray<VibrationEffect> mFallbacks = new SparseArray<>();
+ public final IBinder callerToken;
/** A {@link CountDownLatch} to enable waiting for completion. */
private final CountDownLatch mCompletionLatch = new CountDownLatch(1);
@@ -55,9 +56,10 @@
private int mScaleLevel;
private float mAdaptiveScale;
- HalVibration(@NonNull IBinder token, @NonNull CombinedVibration effect,
+ HalVibration(@NonNull IBinder callerToken, @NonNull CombinedVibration effect,
@NonNull VibrationSession.CallerInfo callerInfo) {
- super(token, callerInfo);
+ super(callerInfo);
+ this.callerToken = callerToken;
mOriginalEffect = effect;
mEffectToPlay = effect;
mScaleLevel = VibrationScaler.SCALE_NONE;
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 9a04793..21fd4ce 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -53,16 +53,13 @@
public final long id;
public final VibrationSession.CallerInfo callerInfo;
public final VibrationStats stats = new VibrationStats();
- public final IBinder callerToken;
private VibrationSession.Status mStatus;
- Vibration(@NonNull IBinder token, @NonNull VibrationSession.CallerInfo callerInfo) {
- Objects.requireNonNull(token);
+ Vibration(@NonNull VibrationSession.CallerInfo callerInfo) {
Objects.requireNonNull(callerInfo);
mStatus = VibrationSession.Status.RUNNING;
this.id = sNextVibrationId.getAndIncrement();
- this.callerToken = token;
this.callerInfo = callerInfo;
}
diff --git a/services/core/java/com/android/server/vibrator/VibrationSession.java b/services/core/java/com/android/server/vibrator/VibrationSession.java
index 5640b49..70477a2 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSession.java
@@ -49,14 +49,26 @@
* Links this session to the app process death with given callback to handle it.
*
* <p>This can be used by the service to end the vibration session when the app process dies.
+ *
+ * @param callback The service callback to be triggered when the binder dies
+ * @return true if the link was successful, false otherwise
*/
- void linkToDeath(Runnable callback);
+ boolean linkToDeath(@Nullable Runnable callback);
/** Removes link to the app process death. */
void unlinkToDeath();
- /** Notify the session end was requested, which might be acted upon asynchronously. */
- void notifyEnded();
+ /**
+ * Notify the session end was requested, which might be acted upon asynchronously.
+ *
+ * <p>Only the first end signal will be used to end a session, but subsequent calls with
+ * {@code immediate} flag set to true can still force it to take effect urgently.
+ *
+ * @param status the end status.
+ * @param endedBy the {@link CallerInfo} of the session that requested this session to end.
+ * @param immediate indicates whether cancellation should abort urgently and skip cleanup steps.
+ */
+ void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy, boolean immediate);
/**
* Session status with reference to values from vibratormanagerservice.proto for logging.
@@ -119,7 +131,7 @@
public final String reason;
CallerInfo(@NonNull VibrationAttributes attrs, int uid, int deviceId, String opPkg,
- String reason) {
+ @Nullable String reason) {
Objects.requireNonNull(attrs);
this.attrs = attrs;
this.uid = uid;
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index 69cdcf4..9cb8c1a 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -31,6 +31,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
+import android.app.IActivityManager;
import android.app.SynchronousUserSwitchObserver;
import android.app.UidObserver;
import android.content.BroadcastReceiver;
@@ -74,6 +75,7 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
/** Controls all the system settings related to vibration. */
@@ -147,9 +149,6 @@
PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE,
PowerManager.GO_TO_SLEEP_REASON_TIMEOUT));
- private static final IntentFilter INTERNAL_RINGER_MODE_CHANGED_INTENT_FILTER =
- new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
-
/** Listener for changes on vibration settings. */
interface OnVibratorSettingsChanged {
/** Callback triggered when any of the vibrator settings change. */
@@ -158,15 +157,18 @@
private final Object mLock = new Object();
private final Context mContext;
- private final String mSystemUiPackage;
@VisibleForTesting
final SettingsContentObserver mSettingObserver;
@VisibleForTesting
- final SettingsBroadcastReceiver mSettingChangeReceiver;
+ final RingerModeBroadcastReceiver mRingerModeBroadcastReceiver;
+ @VisibleForTesting
+ final BatteryBroadcastReceiver mBatteryBroadcastReceiver;
@VisibleForTesting
final VibrationUidObserver mUidObserver;
@VisibleForTesting
final VibrationUserSwitchObserver mUserSwitchObserver;
+ @VisibleForTesting
+ final VibrationLowPowerModeListener mLowPowerModeListener;
@GuardedBy("mLock")
private final List<OnVibratorSettingsChanged> mListeners = new ArrayList<>();
@@ -180,10 +182,13 @@
@GuardedBy("mLock")
@Nullable
private PowerManagerInternal mPowerManagerInternal;
+ @GuardedBy("mLock")
@Nullable
private VirtualDeviceManagerInternal mVirtualDeviceManagerInternal;
@GuardedBy("mLock")
+ private String mSystemUiPackage;
+ @GuardedBy("mLock")
private boolean mVibrateInputDevices;
@GuardedBy("mLock")
private SparseIntArray mCurrentVibrationIntensities = new SparseIntArray();
@@ -205,11 +210,11 @@
mContext = context;
mVibrationConfig = config;
mSettingObserver = new SettingsContentObserver(handler);
- mSettingChangeReceiver = new SettingsBroadcastReceiver();
+ mRingerModeBroadcastReceiver = new RingerModeBroadcastReceiver();
+ mBatteryBroadcastReceiver = new BatteryBroadcastReceiver();
mUidObserver = new VibrationUidObserver();
mUserSwitchObserver = new VibrationUserSwitchObserver();
- mSystemUiPackage = LocalServices.getService(PackageManagerInternal.class)
- .getSystemUiServiceComponent().getPackageName();
+ mLowPowerModeListener = new VibrationLowPowerModeListener();
VibrationEffect clickEffect = createEffectFromResource(
com.android.internal.R.array.config_virtualKeyVibePattern);
@@ -233,18 +238,34 @@
}
public void onSystemReady() {
- PowerManagerInternal pm = LocalServices.getService(PowerManagerInternal.class);
- AudioManager am = mContext.getSystemService(AudioManager.class);
- int ringerMode = am.getRingerModeInternal();
+ onSystemReady(LocalServices.getService(PackageManagerInternal.class),
+ LocalServices.getService(PowerManagerInternal.class),
+ ActivityManager.getService(),
+ LocalServices.getService(VirtualDeviceManagerInternal.class),
+ mContext.getSystemService(AudioManager.class));
+ }
+
+ @VisibleForTesting
+ void onSystemReady(PackageManagerInternal packageManagerInternal,
+ PowerManagerInternal powerManagerInternal,
+ IActivityManager activityManagerInternal,
+ @Nullable VirtualDeviceManagerInternal virtualDeviceManagerInternal,
+ @Nullable AudioManager audioManager) {
+ int ringerMode = (audioManager == null)
+ ? AudioManager.RINGER_MODE_NORMAL
+ : audioManager.getRingerModeInternal();
+ String sysUiPackage = packageManagerInternal.getSystemUiServiceComponent().getPackageName();
synchronized (mLock) {
- mPowerManagerInternal = pm;
- mAudioManager = am;
+ mPowerManagerInternal = powerManagerInternal;
+ mVirtualDeviceManagerInternal = virtualDeviceManagerInternal;
+ mAudioManager = audioManager;
mRingerMode = ringerMode;
+ mSystemUiPackage = sysUiPackage;
}
try {
- ActivityManager.getService().registerUidObserver(mUidObserver,
+ activityManagerInternal.registerUidObserver(mUidObserver,
ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE,
ActivityManager.PROCESS_STATE_UNKNOWN, /* callingPackage= */ null);
} catch (RemoteException e) {
@@ -252,32 +273,16 @@
}
try {
- ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);
+ activityManagerInternal.registerUserSwitchObserver(mUserSwitchObserver, TAG);
} catch (RemoteException e) {
// ignored; both services live in system_server
}
- pm.registerLowPowerModeObserver(
- new PowerManagerInternal.LowPowerModeListener() {
- @Override
- public int getServiceType() {
- return PowerManager.ServiceType.VIBRATION;
- }
+ powerManagerInternal.registerLowPowerModeObserver(mLowPowerModeListener);
- @Override
- public void onLowPowerModeChanged(PowerSaveState result) {
- boolean shouldNotifyListeners;
- synchronized (mLock) {
- shouldNotifyListeners = result.batterySaverEnabled != mBatterySaverMode;
- mBatterySaverMode = result.batterySaverEnabled;
- }
- if (shouldNotifyListeners) {
- notifyListeners();
- }
- }
- });
-
- registerSettingsChangeReceiver(INTERNAL_RINGER_MODE_CHANGED_INTENT_FILTER);
+ mContext.registerReceiver(mRingerModeBroadcastReceiver,
+ new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION),
+ Context.RECEIVER_EXPORTED_UNAUDITED);
// Listen to all settings that might affect the result of Vibrator.getVibrationIntensity.
registerSettingsObserver(Settings.System.getUriFor(Settings.System.VIBRATE_INPUT_DEVICES));
@@ -301,12 +306,7 @@
if (mVibrationConfig.ignoreVibrationsOnWirelessCharger()) {
Intent batteryStatus = mContext.registerReceiver(
- new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateBatteryInfo(intent);
- }
- },
+ mBatteryBroadcastReceiver,
new IntentFilter(Intent.ACTION_BATTERY_CHANGED),
Context.RECEIVER_NOT_EXPORTED);
// After registering the receiver for battery status, process the sticky broadcast that
@@ -476,8 +476,10 @@
public boolean shouldCancelVibrationOnScreenOff(@NonNull CallerInfo callerInfo,
long vibrationStartUptimeMillis) {
PowerManagerInternal pm;
+ String sysUiPackageName;
synchronized (mLock) {
pm = mPowerManagerInternal;
+ sysUiPackageName = mSystemUiPackage;
}
if (pm != null) {
// The SleepData from PowerManager may refer to a more recent sleep than the broadcast
@@ -501,7 +503,7 @@
}
// Only allow vibrations from System packages to continue vibrating when the screen goes off
return callerInfo.uid != Process.SYSTEM_UID && callerInfo.uid != 0
- && !mSystemUiPackage.equals(callerInfo.opPkg);
+ && !Objects.equals(sysUiPackageName, callerInfo.opPkg);
}
/**
@@ -782,11 +784,6 @@
UserHandle.USER_ALL);
}
- private void registerSettingsChangeReceiver(IntentFilter intentFilter) {
- mContext.registerReceiver(mSettingChangeReceiver, intentFilter,
- Context.RECEIVER_EXPORTED_UNAUDITED);
- }
-
@Nullable
private VibrationEffect createEffectFromResource(int resId) {
return createEffectFromResource(mContext.getResources(), resId);
@@ -833,12 +830,11 @@
}
private boolean isAppRunningOnAnyVirtualDevice(int uid) {
- if (mVirtualDeviceManagerInternal == null) {
- mVirtualDeviceManagerInternal =
- LocalServices.getService(VirtualDeviceManagerInternal.class);
+ VirtualDeviceManagerInternal vdm;
+ synchronized (mLock) {
+ vdm = mVirtualDeviceManagerInternal;
}
- return mVirtualDeviceManagerInternal != null
- && mVirtualDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(uid);
+ return vdm != null && vdm.isAppRunningOnAnyVirtualDevice(uid);
}
/** Implementation of {@link ContentObserver} to be registered to a setting {@link Uri}. */
@@ -857,7 +853,7 @@
/** Implementation of {@link BroadcastReceiver} to update on ringer mode change. */
@VisibleForTesting
- final class SettingsBroadcastReceiver extends BroadcastReceiver {
+ final class RingerModeBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
@@ -868,6 +864,18 @@
}
}
+ /** Implementation of {@link BroadcastReceiver} to update on battery mode change. */
+ @VisibleForTesting
+ final class BatteryBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
+ updateBatteryInfo(intent);
+ }
+ }
+ }
+
/** Implementation of {@link ContentObserver} to be registered to a setting {@link Uri}. */
@VisibleForTesting
final class VibrationUidObserver extends UidObserver {
@@ -913,4 +921,25 @@
update();
}
}
+
+ /** Implementation of {@link PowerManagerInternal.LowPowerModeListener} for low battery. */
+ @VisibleForTesting
+ final class VibrationLowPowerModeListener implements PowerManagerInternal.LowPowerModeListener {
+ @Override
+ public int getServiceType() {
+ return PowerManager.ServiceType.VIBRATION;
+ }
+
+ @Override
+ public void onLowPowerModeChanged(PowerSaveState result) {
+ boolean shouldNotifyListeners;
+ synchronized (mLock) {
+ shouldNotifyListeners = result.batterySaverEnabled != mBatterySaverMode;
+ mBatterySaverMode = result.batterySaverEnabled;
+ }
+ if (shouldNotifyListeners) {
+ notifyListeners();
+ }
+ }
+ }
}
diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
index 5137d19..1d52e3c 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java
@@ -21,6 +21,7 @@
import android.os.Build;
import android.os.CombinedVibration;
import android.os.IBinder;
+import android.os.RemoteException;
import android.os.VibrationEffect;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
@@ -38,6 +39,7 @@
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
+import java.util.NoSuchElementException;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.concurrent.CancellationException;
@@ -358,6 +360,28 @@
}
/**
+ * Returns true if successfully linked this conductor to the death of the binder that requested
+ * the vibration.
+ */
+ public boolean linkToDeath() {
+ try {
+ mVibration.callerToken.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error linking vibration to token death", e);
+ return false;
+ }
+ return true;
+ }
+
+ public void unlinkToDeath() {
+ try {
+ mVibration.callerToken.unlinkToDeath(this, 0);
+ } catch (NoSuchElementException e) {
+ Slog.wtf(TAG, "Failed to unlink vibration to token death", e);
+ }
+ }
+
+ /**
* Notify the execution that cancellation is requested. This will be acted upon
* asynchronously in the VibrationThread.
*
@@ -452,6 +476,23 @@
}
}
+ /**
+ * Notify that the VibrationThread has completed the vibration effect playback.
+ *
+ * <p>This is a lightweight method intended to be called by the vibration thread directly. The
+ * VibrationThread may still be continuing with cleanup tasks, and should not be given new work
+ * until it notifies the manager that it has been released.
+ */
+ public void notifyVibrationComplete(@NonNull Vibration.EndInfo endInfo) {
+ if (Build.IS_DEBUGGABLE) {
+ expectIsVibrationThread(true);
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "Vibration " + mVibration.id + " finished with " + endInfo);
+ }
+ mVibration.end(endInfo);
+ }
+
/** Returns true if a cancellation signal was sent via {@link #notifyCancelled}. */
public boolean wasNotifiedToCancel() {
if (Build.IS_DEBUGGABLE) {
diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 4c1e16c..5b22c10 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -16,12 +16,12 @@
package com.android.server.vibrator;
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process;
-import android.os.RemoteException;
import android.os.SystemClock;
import android.os.Trace;
import android.os.WorkSource;
@@ -31,7 +31,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.vibrator.VibrationSession.Status;
-import java.util.NoSuchElementException;
import java.util.Objects;
/** Plays a {@link HalVibration} in dedicated thread. */
@@ -72,14 +71,6 @@
void noteVibratorOff(int uid);
/**
- * Tell the manager that the currently active vibration has completed its vibration, from
- * the perspective of the Effect. However, the VibrationThread may still be continuing with
- * cleanup tasks, and should not be given new work until {@link #onVibrationThreadReleased}
- * is called.
- */
- void onVibrationCompleted(long vibrationId, @NonNull Vibration.EndInfo vibrationEndInfo);
-
- /**
* Tells the manager that the VibrationThread is finished with the previous vibration and
* all of its cleanup tasks, and the vibrators can now be used for another vibration.
*/
@@ -128,7 +119,7 @@
* before the release callback.
*/
boolean runVibrationOnVibrationThread(VibrationStepConductor conductor) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread");
try {
synchronized (mLock) {
if (mRequestedActiveConductor != null) {
@@ -140,7 +131,7 @@
}
return true;
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -243,7 +234,7 @@
mWakeLock.acquire();
try {
try {
- runCurrentVibrationWithWakeLockAndDeathLink();
+ playVibration();
} finally {
clientVibrationCompleteIfNotAlready(
new Vibration.EndInfo(Status.FINISHED_UNEXPECTED));
@@ -254,46 +245,23 @@
}
}
- /**
- * Runs the VibrationThread with the binder death link, handling link/unlink failures.
- * Called from within runWithWakeLock.
- */
- private void runCurrentVibrationWithWakeLockAndDeathLink() {
- IBinder vibrationBinderToken = mExecutingConductor.getVibration().callerToken;
- try {
- vibrationBinderToken.linkToDeath(mExecutingConductor, 0);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error linking vibration to token death", e);
- clientVibrationCompleteIfNotAlready(
- new Vibration.EndInfo(Status.IGNORED_ERROR_TOKEN));
- return;
- }
- // Ensure that the unlink always occurs now.
- try {
- // This is the actual execution of the vibration.
- playVibration();
- } finally {
- try {
- vibrationBinderToken.unlinkToDeath(mExecutingConductor, 0);
- } catch (NoSuchElementException e) {
- Slog.wtf(TAG, "Failed to unlink token", e);
- }
- }
- }
-
// Indicate that the vibration is complete. This can be called multiple times only for
// convenience of handling error conditions - an error after the client is complete won't
// affect the status.
private void clientVibrationCompleteIfNotAlready(@NonNull Vibration.EndInfo vibrationEndInfo) {
if (!mCalledVibrationCompleteCallback) {
mCalledVibrationCompleteCallback = true;
- mVibratorManagerHooks.onVibrationCompleted(
- mExecutingConductor.getVibration().id, vibrationEndInfo);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "notifyVibrationComplete");
+ try {
+ mExecutingConductor.notifyVibrationComplete(vibrationEndInfo);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
+ }
}
}
private void playVibration() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "playVibration");
try {
mExecutingConductor.prepareToStart();
while (!mExecutingConductor.isFinished()) {
@@ -317,7 +285,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
}
diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java
index 3c47850..c120fc7 100644
--- a/services/core/java/com/android/server/vibrator/VibratorController.java
+++ b/services/core/java/com/android/server/vibrator/VibratorController.java
@@ -16,6 +16,8 @@
package com.android.server.vibrator;
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
import android.annotation.Nullable;
import android.hardware.vibrator.IVibrator;
import android.os.Binder;
@@ -124,7 +126,7 @@
/** Reruns the query to the vibrator to load the {@link VibratorInfo}, if not yet successful. */
public void reloadVibratorInfoIfNeeded() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#reloadVibratorInfoIfNeeded");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#reloadVibratorInfoIfNeeded");
try {
// Early check outside lock, for quick return.
if (mVibratorInfoLoadSuccessful) {
@@ -143,7 +145,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -199,13 +201,13 @@
/** Return {@code true} if the underlying vibrator is currently available, false otherwise. */
public boolean isAvailable() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#isAvailable");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#isAvailable");
try {
synchronized (mLock) {
return mNativeWrapper.isAvailable();
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -215,7 +217,9 @@
* <p>This will affect the state of {@link #isUnderExternalControl()}.
*/
public void setExternalControl(boolean externalControl) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setExternalControl(" + externalControl + ")");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR,
+ externalControl ? "VibratorController#enableExternalControl"
+ : "VibratorController#disableExternalControl");
try {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_EXTERNAL_CONTROL)) {
return;
@@ -225,7 +229,7 @@
mNativeWrapper.setExternalControl(externalControl);
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -234,7 +238,7 @@
* if given {@code effect} is {@code null}.
*/
public void updateAlwaysOn(int id, @Nullable PrebakedSegment prebaked) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#updateAlwaysOn");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#updateAlwaysOn");
try {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_ALWAYS_ON_CONTROL)) {
return;
@@ -248,13 +252,13 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
/** Set the vibration amplitude. This will NOT affect the state of {@link #isVibrating()}. */
public void setAmplitude(float amplitude) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#setAmplitude");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#setAmplitude");
try {
synchronized (mLock) {
if (mVibratorInfo.hasCapability(IVibrator.CAP_AMPLITUDE_CONTROL)) {
@@ -265,7 +269,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -279,7 +283,7 @@
* do not support the input or a negative number if the operation failed.
*/
public long on(long milliseconds, long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on");
try {
synchronized (mLock) {
long duration = mNativeWrapper.on(milliseconds, vibrationId);
@@ -290,7 +294,7 @@
return duration;
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -304,7 +308,7 @@
* do not support the input or a negative number if the operation failed.
*/
public long on(VibrationEffect.VendorEffect vendorEffect, long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (vendor)");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (vendor)");
synchronized (mLock) {
Parcel vendorData = Parcel.obtain();
try {
@@ -320,7 +324,7 @@
return duration;
} finally {
vendorData.recycle();
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
}
@@ -335,7 +339,7 @@
* do not support the input or a negative number if the operation failed.
*/
public long on(PrebakedSegment prebaked, long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (Prebaked)");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (Prebaked)");
try {
synchronized (mLock) {
long duration = mNativeWrapper.perform(prebaked.getEffectId(),
@@ -347,7 +351,7 @@
return duration;
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -361,7 +365,7 @@
* do not support the input or a negative number if the operation failed.
*/
public long on(PrimitiveSegment[] primitives, long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (Primitive)");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (Primitive)");
try {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
return 0;
@@ -375,7 +379,7 @@
return duration;
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -388,7 +392,7 @@
* @return The duration of the effect playing, or 0 if unsupported.
*/
public long on(RampSegment[] primitives, long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE)");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE)");
try {
if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS)) {
return 0;
@@ -403,7 +407,7 @@
return duration;
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -413,7 +417,7 @@
* <p>This will affect the state of {@link #isVibrating()}.
*/
public void off() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "VibratorController#off");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#off");
try {
synchronized (mLock) {
mNativeWrapper.off();
@@ -421,7 +425,7 @@
notifyListenerOnVibrating(false);
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index a76d8d6..95c6483 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.vibrator;
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
import static android.os.VibrationAttributes.USAGE_CLASS_ALARM;
import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
@@ -333,7 +334,7 @@
@VisibleForTesting
void systemReady() {
Slog.v(TAG, "Initializing VibratorManager service...");
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "systemReady");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "systemReady");
try {
// Will retry to load each vibrator's info, if any request have failed.
for (int i = 0; i < mVibrators.size(); i++) {
@@ -352,7 +353,7 @@
mServiceReady = true;
}
Slog.v(TAG, "VibratorManager service initialized");
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -413,7 +414,7 @@
@Override // Binder call
public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId,
@Nullable CombinedVibration effect, @Nullable VibrationAttributes attrs) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "setAlwaysOnEffect");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "setAlwaysOnEffect");
try {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.VIBRATE_ALWAYS_ON,
@@ -449,20 +450,25 @@
}
return true;
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override // Binder call
public void vibrate(int uid, int deviceId, String opPkg, @NonNull CombinedVibration effect,
@Nullable VibrationAttributes attrs, String reason, IBinder token) {
- vibrateWithPermissionCheck(uid, deviceId, opPkg, effect, attrs, reason, token);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "vibrate");
+ try {
+ vibrateWithPermissionCheck(uid, deviceId, opPkg, effect, attrs, reason, token);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
+ }
}
@Override // Binder call
public void performHapticFeedback(int uid, int deviceId, String opPkg, int constant,
String reason, int flags, int privFlags) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedback");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedback");
// Note that the `performHapticFeedback` method does not take a token argument from the
// caller, and instead, uses this service as the token. This is to mitigate performance
// impact that would otherwise be caused due to marshal latency. Haptic feedback effects are
@@ -471,7 +477,7 @@
performHapticFeedbackInternal(uid, deviceId, opPkg, constant, reason, /* token= */
this, flags, privFlags);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -479,13 +485,13 @@
public void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg,
int constant, int inputDeviceId, int inputSource, String reason, int flags,
int privFlags) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice");
try {
performHapticFeedbackForInputDeviceInternal(uid, deviceId, opPkg, constant,
inputDeviceId,
inputSource, reason, /* token= */ this, flags, privFlags);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -563,26 +569,16 @@
HalVibration vibrateWithPermissionCheck(int uid, int deviceId, String opPkg,
@NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs,
String reason, IBinder token) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason = " + reason);
- try {
- attrs = fixupVibrationAttributes(attrs, effect);
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.VIBRATE, "vibrate");
- return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
+ attrs = fixupVibrationAttributes(attrs, effect);
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.VIBRATE, "vibrate");
+ return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
}
HalVibration vibrateWithoutPermissionCheck(int uid, int deviceId, String opPkg,
@NonNull CombinedVibration effect, @NonNull VibrationAttributes attrs,
String reason, IBinder token) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate no perm check, reason = " + reason);
- try {
- return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
+ return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token);
}
private HalVibration vibrateInternal(int uid, int deviceId, String opPkg,
@@ -633,12 +629,9 @@
final long ident = Binder.clearCallingIdentity();
try {
if (mCurrentExternalVibration != null) {
- mCurrentExternalVibration.notifyEnded();
vib.stats.reportInterruptedAnotherVibration(
- mCurrentExternalVibration.callerInfo);
- endExternalVibrateLocked(
- new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
- vib.callerInfo),
+ mCurrentExternalVibration.getCallerInfo());
+ endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED, vib.callerInfo,
/* continueExternalControl= */ false);
} else if (mCurrentVibration != null) {
if (mCurrentVibration.getVibration().canPipelineWith(vib)) {
@@ -666,7 +659,7 @@
// Ignored or failed to start the vibration, end it and report metrics right away.
if (vibrationEndInfo != null) {
- endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ true);
+ endVibrationLocked(vib, vibrationEndInfo);
}
return vib;
}
@@ -674,7 +667,7 @@
@Override // Binder call
public void cancelVibrate(int usageFilter, IBinder token) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelVibrate");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "cancelVibrate");
try {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.VIBRATE,
@@ -703,16 +696,15 @@
&& shouldCancelVibration(
mCurrentExternalVibration.getCallerInfo().attrs,
usageFilter)) {
- mCurrentExternalVibration.notifyEnded();
- endExternalVibrateLocked(
- cancelledByUserInfo, /* continueExternalControl= */ false);
+ endExternalVibrateLocked(cancelledByUserInfo.status,
+ cancelledByUserInfo.endedBy, /* continueExternalControl= */ false);
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -903,7 +895,7 @@
@GuardedBy("mLock")
@Nullable
private Vibration.EndInfo startVibrationLocked(HalVibration vib) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "startVibrationLocked");
try {
if (mInputDeviceDelegate.isAvailable()) {
return startVibrationOnInputDevicesLocked(vib);
@@ -923,7 +915,7 @@
mNextVibration = conductor;
return null;
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -934,9 +926,14 @@
int mode = startAppOpModeLocked(vib.callerInfo);
switch (mode) {
case AppOpsManager.MODE_ALLOWED:
- Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
+ Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0);
// Make sure mCurrentVibration is set while triggering the VibrationThread.
mCurrentVibration = conductor;
+ if (!mCurrentVibration.linkToDeath()) {
+ // Shouldn't happen. The method call already logs a wtf.
+ mCurrentVibration = null; // Aborted.
+ return new Vibration.EndInfo(Status.IGNORED_ERROR_TOKEN);
+ }
if (!mVibrationThread.runVibrationOnVibrationThread(mCurrentVibration)) {
// Shouldn't happen. The method call already logs a wtf.
mCurrentVibration = null; // Aborted.
@@ -953,14 +950,21 @@
}
@GuardedBy("mLock")
- private void endVibrationLocked(Vibration vib, Vibration.EndInfo vibrationEndInfo,
- boolean shouldWriteStats) {
+ private void endVibrationLocked(Vibration vib, Status status) {
+ endVibrationLocked(vib, new Vibration.EndInfo(status));
+ }
+
+ @GuardedBy("mLock")
+ private void endVibrationLocked(Vibration vib, Vibration.EndInfo vibrationEndInfo) {
vib.end(vibrationEndInfo);
+ reportEndedVibrationLocked(vib);
+ }
+
+ @GuardedBy("mLock")
+ private void reportEndedVibrationLocked(Vibration vib) {
logAndRecordVibration(vib.getDebugInfo());
- if (shouldWriteStats) {
- mFrameworkStatsLogger.writeVibrationReportedAsync(
- vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
- }
+ mFrameworkStatsLogger.writeVibrationReportedAsync(
+ vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis()));
}
private VibrationStepConductor createVibrationStepConductor(HalVibration vib) {
@@ -1055,17 +1059,15 @@
}
@GuardedBy("mLock")
- private void reportFinishedVibrationLocked(Vibration.EndInfo vibrationEndInfo) {
+ private void reportFinishedVibrationLocked() {
Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
+ mCurrentVibration.unlinkToDeath();
HalVibration vib = mCurrentVibration.getVibration();
if (DEBUG) {
- Slog.d(TAG, "Reporting vibration " + vib.id + " finished with "
- + vibrationEndInfo);
+ Slog.d(TAG, "Reporting vibration " + vib.id + " finished with " + vib.getStatus());
}
- // DO NOT write metrics at this point, wait for the VibrationThread to report the
- // vibration was released, after all cleanup. The metrics will be reported then.
- endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ false);
finishAppOpModeLocked(vib.callerInfo);
+ reportEndedVibrationLocked(vib);
}
private void onSyncedVibrationComplete(long vibrationId) {
@@ -1575,7 +1577,7 @@
@Override
public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "prepareSyncedVibration");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "prepareSyncedVibration");
try {
if ((mCapabilities & requiredCapabilities) != requiredCapabilities) {
// This sync step requires capabilities this device doesn't have, skipping
@@ -1584,33 +1586,33 @@
}
return mNativeWrapper.prepareSynced(vibratorIds);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override
public boolean triggerSyncedVibration(long vibrationId) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "triggerSyncedVibration");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "triggerSyncedVibration");
try {
return mNativeWrapper.triggerSynced(vibrationId);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override
public void cancelSyncedVibration() {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelSyncedVibration");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "cancelSyncedVibration");
try {
mNativeWrapper.cancelSynced();
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override
public void noteVibratorOn(int uid, long duration) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOn");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "noteVibratorOn");
try {
if (duration <= 0) {
// Tried to turn vibrator ON and got:
@@ -1629,38 +1631,20 @@
} catch (RemoteException e) {
Slog.e(TAG, "Error logging VibratorStateChanged to ON", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override
public void noteVibratorOff(int uid) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOff");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "noteVibratorOff");
try {
mBatteryStatsService.noteVibratorOff(uid);
mFrameworkStatsLogger.writeVibratorStateOffAsync(uid);
} catch (RemoteException e) {
Slog.e(TAG, "Error logging VibratorStateChanged to OFF", e);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
- }
- }
-
- @Override
- public void onVibrationCompleted(long vibrationId, Vibration.EndInfo vibrationEndInfo) {
- if (DEBUG) {
- Slog.d(TAG, "Vibration " + vibrationId + " finished with " + vibrationEndInfo);
- }
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationCompleted");
- try {
- synchronized (mLock) {
- if (mCurrentVibration != null
- && mCurrentVibration.getVibration().id == vibrationId) {
- reportFinishedVibrationLocked(vibrationEndInfo);
- }
- }
- } finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -1669,7 +1653,8 @@
if (DEBUG) {
Slog.d(TAG, "VibrationThread released after finished vibration");
}
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationThreadReleased: " + vibrationId);
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVibrationThreadReleased");
+
try {
synchronized (mLock) {
if (DEBUG) {
@@ -1682,11 +1667,8 @@
mCurrentVibration.getVibration().id, vibrationId));
}
if (mCurrentVibration != null) {
- // This is when we consider the current vibration complete, so report
- // metrics.
- mFrameworkStatsLogger.writeVibrationReportedAsync(
- mCurrentVibration.getVibration().getStatsInfo(
- /* completionUptimeMillis= */ SystemClock.uptimeMillis()));
+ // This is when we consider the current vibration complete, report metrics.
+ reportFinishedVibrationLocked();
mCurrentVibration = null;
}
if (mNextVibration != null) {
@@ -1696,13 +1678,12 @@
nextConductor);
if (vibrationEndInfo != null) {
// Failed to start the vibration, end it and report metrics right away.
- endVibrationLocked(nextConductor.getVibration(),
- vibrationEndInfo, /* shouldWriteStats= */ true);
+ endVibrationLocked(nextConductor.getVibration(), vibrationEndInfo);
}
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
}
@@ -1929,8 +1910,7 @@
+ " with end info: " + vibrationEndInfo);
}
// Clearing next vibration before playing it, end it and report metrics right away.
- endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo,
- /* shouldWriteStats= */ true);
+ endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo);
mNextVibration = null;
}
}
@@ -1938,23 +1918,24 @@
/**
* Ends the external vibration, and clears related service state.
*
- * @param vibrationEndInfo the status and related info to end the associated Vibration
+ * @param status the status to end the associated Vibration
+ * @param endedBy the caller that caused this vibration to end
* @param continueExternalControl indicates whether external control will continue. If not, the
* HAL will have external control turned off.
*/
@GuardedBy("mLock")
- private void endExternalVibrateLocked(Vibration.EndInfo vibrationEndInfo,
+ private void endExternalVibrateLocked(Status status, CallerInfo endedBy,
boolean continueExternalControl) {
if (mCurrentExternalVibration == null) {
return;
}
+ mCurrentExternalVibration.requestEnd(status, endedBy, /* immediate= */ true);
mCurrentExternalVibration.unlinkToDeath();
if (!continueExternalControl) {
setExternalControl(false, mCurrentExternalVibration.stats);
}
// The external control was turned off, end it and report metrics right away.
- endVibrationLocked(mCurrentExternalVibration, vibrationEndInfo,
- /* shouldWriteStats= */ true);
+ reportEndedVibrationLocked(mCurrentExternalVibration);
mCurrentExternalVibration = null;
}
@@ -2010,7 +1991,7 @@
@Override
public ExternalVibrationScale onExternalVibrationStart(ExternalVibration vib) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStart");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStart");
try {
// Create Vibration.Stats as close to the received request as possible, for
// tracking.
@@ -2022,9 +2003,7 @@
synchronized (mLock) {
if (!hasExternalControlCapability()) {
- endVibrationLocked(externalVibration,
- new Vibration.EndInfo(Status.IGNORED_UNSUPPORTED),
- /* shouldWriteStats= */ true);
+ endVibrationLocked(externalVibration, Status.IGNORED_UNSUPPORTED);
return externalVibration.getScale();
}
@@ -2035,9 +2014,7 @@
Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid()
+ " tried to play externally controlled vibration"
+ " without VIBRATE permission, ignoring.");
- endVibrationLocked(externalVibration,
- new Vibration.EndInfo(Status.IGNORED_MISSING_PERMISSION),
- /* shouldWriteStats= */ true);
+ endVibrationLocked(externalVibration, Status.IGNORED_MISSING_PERMISSION);
return externalVibration.getScale();
}
@@ -2058,8 +2035,7 @@
}
if (vibrationEndInfo != null) {
- endVibrationLocked(externalVibration, vibrationEndInfo,
- /* shouldWriteStats= */ true);
+ endVibrationLocked(externalVibration, vibrationEndInfo);
return externalVibration.getScale();
}
@@ -2092,15 +2068,32 @@
// as we would need to mute the old one still if it came from a different
// controller.
alreadyUnderExternalControl = true;
- mCurrentExternalVibration.notifyEnded();
externalVibration.stats.reportInterruptedAnotherVibration(
- mCurrentExternalVibration.callerInfo);
- endExternalVibrateLocked(
- new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
- externalVibration.callerInfo),
- /* continueExternalControl= */ true);
+ mCurrentExternalVibration.getCallerInfo());
+ endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED,
+ externalVibration.callerInfo, /* continueExternalControl= */ true);
}
-
+ }
+ // Wait for lock and interact with HAL to set external control outside main lock.
+ if (waitForCompletion) {
+ if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) {
+ Slog.e(TAG, "Timed out waiting for vibration to cancel");
+ synchronized (mLock) {
+ endVibrationLocked(externalVibration, Status.IGNORED_ERROR_CANCELLING);
+ return externalVibration.getScale();
+ }
+ }
+ }
+ if (!alreadyUnderExternalControl) {
+ if (DEBUG) {
+ Slog.d(TAG, "Vibrator going under external control.");
+ }
+ setExternalControl(true, externalVibration.stats);
+ }
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slog.d(TAG, "Playing external vibration: " + vib);
+ }
VibrationAttributes attrs = fixupVibrationAttributes(
vib.getVibrationAttributes(),
/* effect= */ null);
@@ -2113,43 +2106,20 @@
mCurrentExternalVibration = externalVibration;
externalVibration.linkToDeath(this::onExternalVibrationBinderDied);
externalVibration.scale(mVibrationScaler, attrs.getUsage());
- }
- if (waitForCompletion) {
- if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) {
- Slog.e(TAG, "Timed out waiting for vibration to cancel");
- synchronized (mLock) {
- // Trigger endExternalVibrateLocked to unlink to death recipient.
- endExternalVibrateLocked(
- new Vibration.EndInfo(Status.IGNORED_ERROR_CANCELLING),
- /* continueExternalControl= */ false);
- // Mute the request, vibration will be ignored.
- externalVibration.muteScale();
- }
- return externalVibration.getScale();
- }
+ // Vibrator will start receiving data from external channels after this point.
+ // Report current time as the vibration start time, for debugging.
+ externalVibration.stats.reportStarted();
+ return externalVibration.getScale();
}
- if (!alreadyUnderExternalControl) {
- if (DEBUG) {
- Slog.d(TAG, "Vibrator going under external control.");
- }
- setExternalControl(true, externalVibration.stats);
- }
- if (DEBUG) {
- Slog.d(TAG, "Playing external vibration: " + vib);
- }
- // Vibrator will start receiving data from external channels after this point.
- // Report current time as the vibration start time, for debugging.
- externalVibration.stats.reportStarted();
- return externalVibration.getScale();
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@Override
public void onExternalVibrationStop(ExternalVibration vib) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStop");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStop");
try {
synchronized (mLock) {
if (mCurrentExternalVibration != null
@@ -2157,13 +2127,12 @@
if (DEBUG) {
Slog.d(TAG, "Stopping external vibration: " + vib);
}
- endExternalVibrateLocked(
- new Vibration.EndInfo(Status.FINISHED),
+ endExternalVibrateLocked(Status.FINISHED, /* endedBy= */ null,
/* continueExternalControl= */ false);
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -2182,8 +2151,7 @@
if (DEBUG) {
Slog.d(TAG, "External vibration finished because binder died");
}
- endExternalVibrateLocked(
- new Vibration.EndInfo(Status.CANCELLED_BINDER_DIED),
+ endExternalVibrateLocked(Status.CANCELLED_BINDER_DIED, /* endedBy= */ null,
/* continueExternalControl= */ false);
}
}
@@ -2232,32 +2200,39 @@
@Override
public int onCommand(String cmd) {
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onCommand " + cmd);
try {
if ("list".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: list");
return runListVibrators();
}
if ("synced".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: synced");
return runMono();
}
if ("combined".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: combined");
return runStereo();
}
if ("sequential".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: sequential");
return runSequential();
}
if ("xml".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: xml");
return runXml();
}
if ("cancel".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: cancel");
return runCancel();
}
if ("feedback".equals(cmd)) {
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: feedback");
return runHapticFeedback();
}
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "onCommand: default");
return handleDefaultCommands(cmd);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index 6740153..ab5316f 100644
--- a/services/core/java/com/android/server/webkit/SystemImpl.java
+++ b/services/core/java/com/android/server/webkit/SystemImpl.java
@@ -41,7 +41,8 @@
import com.android.internal.util.XmlUtils;
import com.android.server.LocalServices;
-import com.android.server.PinnerService;
+import com.android.server.pinner.PinnedFile;
+import com.android.server.pinner.PinnerService;
import org.xmlpull.v1.XmlPullParserException;
@@ -318,8 +319,9 @@
if (webviewPinQuota <= 0) {
break;
}
- int bytesPinned = pinnerService.pinFile(apk, webviewPinQuota, appInfo, PIN_GROUP);
- webviewPinQuota -= bytesPinned;
+ PinnedFile pf = pinnerService.pinFile(
+ apk, webviewPinQuota, appInfo, PIN_GROUP, /*pinOptimizedDeps=*/true);
+ webviewPinQuota -= pf.bytesPinned;
}
}
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
index 241390c1..fbf9478 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java
@@ -170,7 +170,7 @@
* </ul>
*/
boolean shouldApplyFreeformTreatmentForCameraCompat() {
- return Flags.cameraCompatForFreeform() && !isChangeEnabled(mActivityRecord,
+ return Flags.enableCameraCompatForDesktopWindowing() && !isChangeEnabled(mActivityRecord,
OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
}
diff --git a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
index 67bfd76..5338c01 100644
--- a/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatCameraPolicy.java
@@ -48,8 +48,9 @@
// without the need to restart the device.
final boolean needsDisplayRotationCompatPolicy =
wmService.mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime();
- final boolean needsCameraCompatFreeformPolicy = Flags.cameraCompatForFreeform()
- && DesktopModeHelper.canEnterDesktopMode(wmService.mContext);
+ final boolean needsCameraCompatFreeformPolicy =
+ Flags.enableCameraCompatForDesktopWindowing()
+ && DesktopModeHelper.canEnterDesktopMode(wmService.mContext);
if (needsDisplayRotationCompatPolicy || needsCameraCompatFreeformPolicy) {
mCameraStateMonitor = new CameraStateMonitor(displayContent, wmService.mH);
mActivityRefresher = new ActivityRefresher(wmService, wmService.mH);
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 2259b5a..515f148 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -1120,7 +1120,9 @@
@Nullable Task targetTask, int launchFlags, int balCode, int callingUid,
int realCallingUid, TaskDisplayArea preferredTaskDisplayArea) {
// BAL Exception allowed in all cases
- if (balCode == BAL_ALLOW_ALLOWLISTED_UID) {
+ if (balCode == BAL_ALLOW_ALLOWLISTED_UID
+ || (android.security.Flags.asmReintroduceGracePeriod()
+ && balCode == BAL_ALLOW_GRACE_PERIOD)) {
return true;
}
@@ -1173,10 +1175,15 @@
ArrayList<Task> visibleTasks = displayArea.getVisibleTasks();
for (int i = 0; i < visibleTasks.size(); i++) {
Task task = visibleTasks.get(i);
- if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) {
- bas.optedIn(task.getTopMostActivity());
- } else {
+ if (android.security.Flags.asmReintroduceGracePeriod()) {
bas = checkTopActivityForAsm(task, callingUid, /*sourceRecord*/null, bas);
+ } else {
+ if (visibleTasks.size() == 1 && task.isActivityTypeHomeOrRecents()) {
+ bas.optedIn(task.getTopMostActivity());
+ } else {
+ bas = checkTopActivityForAsm(
+ task, callingUid, /*sourceRecord*/null, bas);
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
index e3232e0..d6caa1a 100644
--- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
+++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
@@ -124,7 +124,7 @@
*/
@VisibleForTesting
boolean shouldApplyFreeformTreatmentForCameraCompat(@NonNull ActivityRecord activity) {
- return Flags.cameraCompatForFreeform() && !activity.info.isChangeEnabled(
+ return Flags.enableCameraCompatForDesktopWindowing() && !activity.info.isChangeEnabled(
ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT);
}
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 232c3b6..dcf0319 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -188,9 +188,8 @@
* the application did not handle.
*/
@Override
- public KeyEvent dispatchUnhandledKey(
- IBinder focusedToken, KeyEvent event, int policyFlags) {
- return mService.mPolicy.dispatchUnhandledKey(focusedToken, event, policyFlags);
+ public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
+ return mService.mPolicy.interceptUnhandledKey(event, focusedToken);
}
/** Callback to get pointer layer. */
diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java
index ddbfd70..d7dc459 100644
--- a/services/core/java/com/android/server/wm/InputMonitor.java
+++ b/services/core/java/com/android/server/wm/InputMonitor.java
@@ -222,7 +222,8 @@
UserHandle clientUser) {
final InputConsumerImpl existingConsumer = getInputConsumer(name);
if (existingConsumer != null && existingConsumer.mClientUser.equals(clientUser)) {
- throw new IllegalStateException("Existing input consumer found with name: " + name
+ destroyInputConsumer(existingConsumer.mToken);
+ Slog.w(TAG_WM, "Replacing existing input consumer found with name: " + name
+ ", display: " + mDisplayId + ", user: " + clientUser);
}
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 9da848a..bf623b2 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -24,6 +24,8 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -2040,10 +2042,15 @@
final boolean isOtherUndefinedMode = otherWindowingMode == WINDOWING_MODE_UNDEFINED;
// An activity type and windowing mode is compatible if they are the exact same type/mode,
- // or if one of the type/modes is undefined
+ // or if one of the type/modes is undefined. This is with the exception of
+ // freeform/fullscreen where both modes are assumed to be compatible with each other.
final boolean isCompatibleType = activityType == otherActivityType
|| isUndefinedType || isOtherUndefinedType;
final boolean isCompatibleMode = windowingMode == otherWindowingMode
+ || (windowingMode == WINDOWING_MODE_FREEFORM
+ && otherWindowingMode == WINDOWING_MODE_FULLSCREEN)
+ || (windowingMode == WINDOWING_MODE_FULLSCREEN
+ && otherWindowingMode == WINDOWING_MODE_FREEFORM)
|| isUndefinedMode || isOtherUndefinedMode;
return isCompatibleType && isCompatibleMode;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 4c4b4f6..0e3ab63 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1969,6 +1969,13 @@
boolean isReadyForDisplay() {
final boolean parentAndClientVisible = !isParentWindowHidden()
&& mViewVisibility == View.VISIBLE;
+ // TODO(b/338426357): Remove this once the last target using legacy transitions is moved to
+ // shell transitions
+ if (!mTransitionController.isShellTransitionsEnabled()) {
+ return mHasSurface && isVisibleByPolicy() && !mDestroying
+ && ((parentAndClientVisible && mToken.isVisible())
+ || isAnimating(TRANSITION | PARENTS));
+ }
return mHasSurface && isVisibleByPolicy() && !mDestroying && mToken.isVisible()
&& (parentAndClientVisible || isAnimating(TRANSITION | PARENTS));
}
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index 5cd117b..efca902 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -56,6 +56,7 @@
#include <nativehelper/ScopedPrimitiveArray.h>
#include <nativehelper/ScopedUtfChars.h>
#include <server_configurable_flags/get_flags.h>
+#include <ui/LogicalDisplayId.h>
#include <ui/Region.h>
#include <utils/Log.h>
#include <utils/Looper.h>
@@ -64,6 +65,7 @@
#include <atomic>
#include <cinttypes>
+#include <map>
#include <vector>
#include "android_hardware_display_DisplayViewport.h"
@@ -343,7 +345,7 @@
void setTouchpadRightClickZoneEnabled(bool enabled);
void setInputDeviceEnabled(uint32_t deviceId, bool enabled);
void setShowTouches(bool enabled);
- void setInteractive(bool interactive);
+ void setNonInteractiveDisplays(const std::set<ui::LogicalDisplayId>& displayIds);
void reloadCalibration();
void reloadPointerIcons();
void requestPointerCapture(const sp<IBinder>& windowToken, bool enabled);
@@ -508,9 +510,11 @@
// Keycodes to be remapped.
std::map<int32_t /* fromKeyCode */, int32_t /* toKeyCode */> keyRemapping{};
+
+ // Displays which are non-interactive.
+ std::set<ui::LogicalDisplayId> nonInteractiveDisplays;
} mLocked GUARDED_BY(mLock);
- std::atomic<bool> mInteractive;
void updateInactivityTimeoutLocked();
void handleInterceptActions(jint wmActions, nsecs_t when, uint32_t& policyFlags);
void ensureSpriteControllerLocked();
@@ -524,12 +528,13 @@
void forEachPointerControllerLocked(std::function<void(PointerController&)> apply)
REQUIRES(mLock);
PointerIcon loadPointerIcon(JNIEnv* env, ui::LogicalDisplayId displayId, PointerIconStyle type);
+ bool isDisplayInteractive(ui::LogicalDisplayId displayId);
static inline JNIEnv* jniEnv() { return AndroidRuntime::getJNIEnv(); }
};
NativeInputManager::NativeInputManager(jobject serviceObj, const sp<Looper>& looper)
- : mLooper(looper), mInteractive(true) {
+ : mLooper(looper) {
JNIEnv* env = jniEnv();
mServiceObj = env->NewGlobalRef(serviceObj);
@@ -547,9 +552,13 @@
void NativeInputManager::dump(std::string& dump) {
dump += "Input Manager State:\n";
- dump += StringPrintf(INDENT "Interactive: %s\n", toString(mInteractive.load()));
{ // acquire lock
std::scoped_lock _l(mLock);
+ auto logicalDisplayIdToString = [](const ui::LogicalDisplayId& displayId) {
+ return std::to_string(displayId.val());
+ };
+ dump += StringPrintf(INDENT "Display not interactive: %s\n",
+ dumpSet(mLocked.nonInteractiveDisplays, streamableToString).c_str());
dump += StringPrintf(INDENT "System UI Lights Out: %s\n",
toString(mLocked.systemUiLightsOut));
dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed);
@@ -1476,8 +1485,10 @@
mInputManager->getDispatcher().requestPointerCapture(windowToken, enabled);
}
-void NativeInputManager::setInteractive(bool interactive) {
- mInteractive = interactive;
+void NativeInputManager::setNonInteractiveDisplays(
+ const std::set<ui::LogicalDisplayId>& displayIds) {
+ std::scoped_lock _l(mLock);
+ mLocked.nonInteractiveDisplays = displayIds;
}
void NativeInputManager::reloadCalibration() {
@@ -1606,7 +1617,7 @@
// - Ignore untrusted events and pass them along.
// - Ask the window manager what to do with normal events and trusted injected events.
// - For normal events wake and brighten the screen if currently off or dim.
- const bool interactive = mInteractive.load();
+ const bool interactive = isDisplayInteractive(keyEvent.getDisplayId());
if (interactive) {
policyFlags |= POLICY_FLAG_INTERACTIVE;
}
@@ -1644,7 +1655,7 @@
// - No special filtering for injected events required at this time.
// - Filter normal events based on screen state.
// - For normal events brighten (but do not wake) the screen if currently dim.
- const bool interactive = mInteractive.load();
+ const bool interactive = isDisplayInteractive(displayId);
if (interactive) {
policyFlags |= POLICY_FLAG_INTERACTIVE;
}
@@ -1683,6 +1694,24 @@
}
}
+bool NativeInputManager::isDisplayInteractive(ui::LogicalDisplayId displayId) {
+ // If an input event doesn't have an associated id, use the default display id
+ if (displayId == ui::LogicalDisplayId::INVALID) {
+ displayId = ui::LogicalDisplayId::DEFAULT;
+ }
+
+ { // acquire lock
+ std::scoped_lock _l(mLock);
+
+ auto it = mLocked.nonInteractiveDisplays.find(displayId);
+ if (it != mLocked.nonInteractiveDisplays.end()) {
+ return false;
+ }
+ } // release lock
+
+ return true;
+}
+
nsecs_t NativeInputManager::interceptKeyBeforeDispatching(const sp<IBinder>& token,
const KeyEvent& keyEvent,
uint32_t policyFlags) {
@@ -2372,10 +2401,17 @@
im->setShowTouches(enabled);
}
-static void nativeSetInteractive(JNIEnv* env, jobject nativeImplObj, jboolean interactive) {
+static void nativeSetNonInteractiveDisplays(JNIEnv* env, jobject nativeImplObj,
+ jintArray displayIds) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
- im->setInteractive(interactive);
+ const std::vector displayIdsVec = getIntArray(env, displayIds);
+ std::set<ui::LogicalDisplayId> logicalDisplayIds;
+ for (int displayId : displayIdsVec) {
+ logicalDisplayIds.emplace(ui::LogicalDisplayId{displayId});
+ }
+
+ im->setNonInteractiveDisplays(logicalDisplayIds);
}
static void nativeReloadCalibration(JNIEnv* env, jobject nativeImplObj) {
@@ -3021,7 +3057,7 @@
(void*)nativeSetShouldNotifyTouchpadHardwareState},
{"setTouchpadRightClickZoneEnabled", "(Z)V", (void*)nativeSetTouchpadRightClickZoneEnabled},
{"setShowTouches", "(Z)V", (void*)nativeSetShowTouches},
- {"setInteractive", "(Z)V", (void*)nativeSetInteractive},
+ {"setNonInteractiveDisplays", "([I)V", (void*)nativeSetNonInteractiveDisplays},
{"reloadCalibration", "()V", (void*)nativeReloadCalibration},
{"vibrate", "(I[J[III)V", (void*)nativeVibrate},
{"vibrateCombined", "(I[JLandroid/util/SparseArray;II)V", (void*)nativeVibrateCombined},
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 0eafb59..a07facf 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -512,8 +512,6 @@
<xs:annotation name="final"/>
</xs:element>
</xs:sequence>
- <!-- valid value of interpolation if specified: linear -->
- <xs:attribute name="interpolation" type="xs:string" use="optional"/>
</xs:complexType>
<xs:complexType name="brightnessPoint">
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 355b0ab..5309263 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -91,8 +91,6 @@
public class ComprehensiveBrightnessMap {
ctor public ComprehensiveBrightnessMap();
method @NonNull public final java.util.List<com.android.server.display.config.BrightnessPoint> getBrightnessPoint();
- method public String getInterpolation();
- method public void setInterpolation(String);
}
public class Density {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 407a5a6..4e89b85 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -4152,8 +4152,10 @@
}
private void checkAllUsersAreAffiliatedWithDevice() {
- Preconditions.checkCallAuthorization(areAllUsersAffiliatedWithDeviceLocked(),
- "operation not allowed when device has unaffiliated users");
+ synchronized (getLockObject()) {
+ Preconditions.checkCallAuthorization(areAllUsersAffiliatedWithDeviceLocked(),
+ "operation not allowed when device has unaffiliated users");
+ }
}
@Override
@@ -11362,7 +11364,7 @@
if (mOwners.hasDeviceOwner()) {
return false;
}
-
+
final ComponentName profileOwner = getProfileOwnerAsUser(userId);
if (profileOwner == null) {
return false;
@@ -11371,7 +11373,7 @@
if (isManagedProfile(userId)) {
return false;
}
-
+
return true;
}
private void enforceCanQueryLockTaskLocked(ComponentName who, String callerPackageName) {
@@ -18213,6 +18215,7 @@
return false;
}
+ @GuardedBy("getLockObject()")
private boolean areAllUsersAffiliatedWithDeviceLocked() {
return mInjector.binderWithCleanCallingIdentity(() -> {
final List<UserInfo> userInfos = mUserManager.getAliveUsers();
@@ -18310,10 +18313,12 @@
final CallerIdentity caller = getCallerIdentity(admin, packageName);
if (isPermissionCheckFlagEnabled()) {
- Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile()
- || areAllUsersAffiliatedWithDeviceLocked());
- enforcePermission(MANAGE_DEVICE_POLICY_SECURITY_LOGGING, caller.getPackageName(),
- UserHandle.USER_ALL);
+ synchronized (getLockObject()) {
+ Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile()
+ || areAllUsersAffiliatedWithDeviceLocked());
+ enforcePermission(MANAGE_DEVICE_POLICY_SECURITY_LOGGING, caller.getPackageName(),
+ UserHandle.USER_ALL);
+ }
} else {
if (admin != null) {
Preconditions.checkCallAuthorization(
@@ -18325,8 +18330,10 @@
isCallerDelegate(caller, DELEGATION_SECURITY_LOGGING));
}
- Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile()
- || areAllUsersAffiliatedWithDeviceLocked());
+ synchronized (getLockObject()) {
+ Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile()
+ || areAllUsersAffiliatedWithDeviceLocked());
+ }
}
DevicePolicyEventLogger
@@ -19384,11 +19391,13 @@
PolicyDefinition.RESET_PASSWORD_TOKEN,
enforcingAdmin,
userId);
- // TODO(b/369152176): Address difference in behavior regarding addEscrowToken when
- // compared with the else branch.
long tokenHandle = addEscrowToken(
token, currentTokenHandle == null ? 0 : currentTokenHandle, userId);
if (tokenHandle == 0) {
+ mDevicePolicyEngine.removeLocalPolicy(
+ PolicyDefinition.RESET_PASSWORD_TOKEN,
+ enforcingAdmin,
+ userId);
return false;
}
mDevicePolicyEngine.setLocalPolicy(
@@ -24538,7 +24547,8 @@
}
});
}
-
+
+ @GuardedBy("getLockObject()")
private void migrateUserControlDisabledPackagesLocked() {
Binder.withCleanCallingIdentity(() -> {
List<UserInfo> users = mUserManager.getUsers();
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 5cf260a..ce6f1ec 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -205,6 +205,7 @@
import com.android.server.pdb.PersistentDataBlockService;
import com.android.server.people.PeopleService;
import com.android.server.permission.access.AccessCheckingService;
+import com.android.server.pinner.PinnerService;
import com.android.server.pm.ApexManager;
import com.android.server.pm.ApexSystemServiceInfo;
import com.android.server.pm.BackgroundInstallControlService;
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
new file mode 100644
index 0000000..fead05b
--- /dev/null
+++ b/services/supervision/java/com/android/server/supervision/SupervisionManagerInternal.java
@@ -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.server.supervision;
+
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.Bundle;
+
+/**
+ * Local system service interface for {@link SupervisionService}.
+ *
+ * @hide Only for use within Android OS.
+ */
+public abstract class SupervisionManagerInternal {
+ /**
+ * Returns whether supervision is enabled for the specified user
+ *
+ * @param userId The user to retrieve the supervision state for
+ * @return whether the user is supervised
+ */
+ public abstract boolean isSupervisionEnabledForUser(@UserIdInt int userId);
+
+ /**
+ * Sets whether the supervision lock screen should be shown for the specified user
+ *
+ * @param userId The user set the superivision state for
+ * @param enabled Whether or not the superivision lock screen needs to be shown
+ * @param options Optional configuration parameters for the supervision lock screen
+ */
+ public abstract void setSupervisionLockscreenEnabledForUser(
+ @UserIdInt int userId, boolean enabled, @Nullable Bundle options);
+}
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionService.java b/services/supervision/java/com/android/server/supervision/SupervisionService.java
index 7ffd0ec..4c515c1 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionService.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionService.java
@@ -18,14 +18,22 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.app.supervision.ISupervisionManager;
import android.content.Context;
+import android.content.pm.UserInfo;
+import android.os.Bundle;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
+import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
+import com.android.server.pm.UserManagerInternal;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -38,13 +46,25 @@
private final Context mContext;
+ // TODO(b/362756788): Does this need to be a LockGuard lock?
+ private final Object mLockDoNoUseDirectly = new Object();
+
+ @GuardedBy("getLockObject()")
+ private final SparseArray<SupervisionUserData> mUserData = new SparseArray<>();
+
+ private final UserManagerInternal mUserManagerInternal;
+
public SupervisionService(Context context) {
- mContext = context.createAttributionContext("SupervisionService");
+ mContext = context.createAttributionContext(LOG_TAG);
+ mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
+ mUserManagerInternal.addUserLifecycleListener(new UserLifecycleListener());
}
@Override
- public boolean isSupervisionEnabled() {
- return false;
+ public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
+ synchronized (getLockObject()) {
+ return getUserDataLocked(userId).supervisionEnabled;
+ }
}
@Override
@@ -60,11 +80,44 @@
}
@Override
- protected void dump(
- @NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args) {
- if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, fout)) return;
+ protected void dump(@NonNull FileDescriptor fd,
+ @NonNull PrintWriter printWriter, @Nullable String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, printWriter)) return;
- fout.println("Supervision enabled: " + isSupervisionEnabled());
+ try (var pw = new IndentingPrintWriter(printWriter, " ")) {
+ pw.println("SupervisionService state:");
+ pw.increaseIndent();
+
+ var users = mUserManagerInternal.getUsers(false);
+ synchronized (getLockObject()) {
+ for (var user : users) {
+ getUserDataLocked(user.id).dump(pw);
+ pw.println();
+ }
+ }
+ }
+ }
+
+ private Object getLockObject() {
+ return mLockDoNoUseDirectly;
+ }
+
+ @NonNull
+ @GuardedBy("getLockObject()")
+ SupervisionUserData getUserDataLocked(@UserIdInt int userId) {
+ SupervisionUserData data = mUserData.get(userId);
+ if (data == null) {
+ // TODO(b/362790738): Do not create user data for nonexistent users.
+ data = new SupervisionUserData(userId);
+ mUserData.append(userId, data);
+ }
+ return data;
+ }
+
+ void setSupervisionEnabledForUser(@UserIdInt int userId, boolean enabled) {
+ synchronized (getLockObject()) {
+ getUserDataLocked(userId).supervisionEnabled = enabled;
+ }
}
public static class Lifecycle extends SystemService {
@@ -77,7 +130,35 @@
@Override
public void onStart() {
+ publishLocalService(SupervisionManagerInternal.class, mSupervisionService.mInternal);
publishBinderService(Context.SUPERVISION_SERVICE, mSupervisionService);
}
}
+
+ final SupervisionManagerInternal mInternal = new SupervisionManagerInternal() {
+ public boolean isSupervisionEnabledForUser(@UserIdInt int userId) {
+ synchronized (getLockObject()) {
+ return getUserDataLocked(userId).supervisionEnabled;
+ }
+ }
+
+ @Override
+ public void setSupervisionLockscreenEnabledForUser(
+ @UserIdInt int userId, boolean enabled, @Nullable Bundle options) {
+ synchronized (getLockObject()) {
+ SupervisionUserData data = getUserDataLocked(userId);
+ data.supervisionLockScreenEnabled = enabled;
+ data.supervisionLockScreenOptions = options;
+ }
+ }
+ };
+
+ private final class UserLifecycleListener implements UserManagerInternal.UserLifecycleListener {
+ @Override
+ public void onUserRemoved(UserInfo user) {
+ synchronized (getLockObject()) {
+ mUserData.remove(user.id);
+ }
+ }
+ }
}
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
index 3aba24a..2adaae3 100644
--- a/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
+++ b/services/supervision/java/com/android/server/supervision/SupervisionServiceShellCommand.java
@@ -17,8 +17,7 @@
package com.android.server.supervision;
import android.os.ShellCommand;
-
-import java.io.PrintWriter;
+import android.os.UserHandle;
public class SupervisionServiceShellCommand extends ShellCommand {
private final SupervisionService mService;
@@ -32,30 +31,29 @@
if (cmd == null) {
return handleDefaultCommands(null);
}
- final PrintWriter pw = getOutPrintWriter();
switch (cmd) {
- case "help": return help(pw);
- case "is-enabled": return isEnabled(pw);
+ case "enable": return setEnabled(true);
+ case "disable": return setEnabled(false);
default: return handleDefaultCommands(cmd);
}
}
- private int help(PrintWriter pw) {
- pw.println("Supervision service commands:");
- pw.println(" help");
- pw.println(" Prints this help text");
- pw.println(" is-enabled");
- pw.println(" Is supervision enabled");
- return 0;
- }
-
- private int isEnabled(PrintWriter pw) {
- pw.println(mService.isSupervisionEnabled());
+ private int setEnabled(boolean enabled) {
+ final var pw = getOutPrintWriter();
+ final var userId = UserHandle.parseUserArg(getNextArgRequired());
+ mService.setSupervisionEnabledForUser(userId, enabled);
return 0;
}
@Override
public void onHelp() {
- help(getOutPrintWriter());
+ final var pw = getOutPrintWriter();
+ pw.println("Supervision service (supervision) commands:");
+ pw.println(" help");
+ pw.println(" Prints this help text");
+ pw.println(" enable <USER_ID>");
+ pw.println(" Enables supervision for the given user.");
+ pw.println(" disable <USER_ID>");
+ pw.println(" Disables supervision for the given user.");
}
}
diff --git a/services/supervision/java/com/android/server/supervision/SupervisionUserData.java b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
new file mode 100644
index 0000000..5616237
--- /dev/null
+++ b/services/supervision/java/com/android/server/supervision/SupervisionUserData.java
@@ -0,0 +1,45 @@
+/*
+ * 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.supervision;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.Bundle;
+import android.util.IndentingPrintWriter;
+
+/** User specific data, used internally by the {@link SupervisionService}. */
+public class SupervisionUserData {
+ public final @UserIdInt int userId;
+ public boolean supervisionEnabled;
+ public boolean supervisionLockScreenEnabled;
+ @Nullable public Bundle supervisionLockScreenOptions;
+
+ public SupervisionUserData(@UserIdInt int userId) {
+ this.userId = userId;
+ }
+
+ void dump(@NonNull IndentingPrintWriter pw) {
+ pw.println();
+ pw.println("User " + userId + ":");
+ pw.increaseIndent();
+ pw.println("supervisionEnabled: " + supervisionEnabled);
+ pw.println("supervisionLockScreenEnabled: " + supervisionLockScreenEnabled);
+ pw.println("supervisionLockScreenOptions: " + supervisionLockScreenOptions);
+ pw.decreaseIndent();
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
index e5d3153..72cbac3 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
@@ -35,6 +35,7 @@
public final class InputMethodManagerServiceTests {
static final int SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 2;
static final int NO_SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 3;
+ private static final int TEST_IME_USER_ID = 1;
static InputMethodManagerService.ImeDisplayValidator sChecker =
(displayId) -> {
@@ -102,7 +103,8 @@
null,
null,
null,
- null));
+ null,
+ TEST_IME_USER_ID));
history.dump(new PrintWriter(writer), "" /* prefix */);
diff --git a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
index 5c4716d..7d5532f 100644
--- a/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
+++ b/services/tests/PackageManagerComponentOverrideTests/src/com/android/server/pm/test/override/PackageManagerComponentLabelIconOverrideTest.kt
@@ -57,6 +57,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doReturn
@@ -383,6 +384,10 @@
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)) {
PackageManager.PERMISSION_GRANTED
}
+ whenever(this.checkPermission(
+ eq(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL), anyInt(), anyInt())) {
+ PackageManager.PERMISSION_GRANTED
+ }
}
val mockSharedLibrariesImpl: SharedLibrariesImpl = mock {
whenever(this.snapshot()) { this@mock }
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
index b21c349..2144785 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/pkg/PackageStateTest.kt
@@ -94,6 +94,7 @@
ParsedService::getIntents,
ParsedService::getProperties,
Intent::getCategories,
+ Intent::getExtraIntentKeys,
PackageUserState::getDisabledComponents,
PackageUserState::getEnabledComponents,
PackageUserState::getSharedLibraryOverlayPaths,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index fd05b26..8e1be9a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -392,7 +392,7 @@
public void testInvalidLuxThrottling() throws Exception {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getInvalidLuxThrottling(), getValidProxSensor(),
- /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
+ /* includeIdleMode= */ true, /* enableEvenDimmer= */ false));
Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
mDisplayDeviceConfig.getLuxThrottlingData();
@@ -600,7 +600,7 @@
public void testProximitySensorWithEmptyValuesFromDisplayConfig() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getValidLuxThrottling(), getProxSensorWithEmptyValues(),
- /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
+ /* includeIdleMode= */ true, /* enableEvenDimmer= */ false));
assertNull(mDisplayDeviceConfig.getProximitySensor());
}
@@ -608,7 +608,7 @@
public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(),
- /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
+ /* includeIdleMode= */ true, /* enableEvenDimmer= */ false));
assertEquals("test_proximity_sensor",
mDisplayDeviceConfig.getProximitySensor().type);
assertEquals("Test Proximity Sensor",
@@ -803,7 +803,7 @@
@Test
public void testBrightnessRamps_IdleFallsBackToConfigInteractive() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
@@ -820,14 +820,14 @@
@Test
public void testBrightnessCapForWearBedtimeMode() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
assertEquals(0.1f, mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA);
}
@Test
public void testAutoBrightnessBrighteningLevels() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
assertArrayEquals(new float[]{0.0f, 80},
mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
@@ -890,7 +890,7 @@
when(mFlags.areAutoBrightnessModesEnabled()).thenReturn(false);
setupDisplayDeviceConfigFromConfigResourceFile();
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ false));
assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
brightnessIntToFloat(150)},
@@ -929,7 +929,7 @@
when(mFlags.isEvenDimmerEnabled()).thenReturn(true);
when(mResources.getBoolean(R.bool.config_evenDimmerEnabled)).thenReturn(true);
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ true));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer= */ true));
assertTrue(mDisplayDeviceConfig.isEvenDimmerAvailable());
assertEquals(0.01f, mDisplayDeviceConfig.getBacklightFromBrightness(0.002f), ZERO_DELTA);
@@ -1365,7 +1365,7 @@
private String getContent() {
return getContent(getValidLuxThrottling(), getValidProxSensor(),
- /* includeIdleMode= */ true, false);
+ /* includeIdleMode= */ true, /* enableEvenDimmer= */ false);
}
private String getContent(String brightnessCapConfig, String proxSensor,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 120cc84..f5bed99 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -29,8 +29,10 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -45,6 +47,7 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.util.Spline;
import android.view.Display;
import android.view.DisplayAddress;
import android.view.SurfaceControl;
@@ -59,6 +62,7 @@
import com.android.internal.R;
import com.android.server.LocalServices;
import com.android.server.display.LocalDisplayAdapter.BacklightAdapter;
+import com.android.server.display.color.ColorDisplayService;
import com.android.server.display.feature.DisplayManagerFlags;
import com.android.server.display.mode.DisplayModeDirector;
import com.android.server.display.notifications.DisplayNotificationManager;
@@ -119,6 +123,8 @@
private DisplayManagerFlags mFlags;
@Mock
private DisplayPowerController mMockedDisplayPowerController;
+ @Mock
+ private ColorDisplayService.ColorDisplayServiceInternal mMockedColorDisplayServiceInternal;
private Handler mHandler;
@@ -133,6 +139,11 @@
private Injector mInjector;
@Mock
+ private DisplayDeviceConfig mMockDisplayDeviceConfig;
+ @Mock
+ private BacklightAdapter mMockBacklightAdapter;
+
+ @Mock
private LocalDisplayAdapter.SurfaceControlProxy mSurfaceControlProxy;
private static final float[] DISPLAY_RANGE_NITS = { 2.685f, 478.5f };
private static final int[] BACKLIGHT_RANGE = { 1, 255 };
@@ -150,6 +161,9 @@
doReturn(mMockedResources).when(mMockedContext).getResources();
LocalServices.removeServiceForTest(LightsManager.class);
LocalServices.addService(LightsManager.class, mMockedLightsManager);
+ LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
+ LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class,
+ mMockedColorDisplayServiceInternal);
mInjector = new Injector();
when(mSurfaceControlProxy.getBootDisplayModeSupport()).thenReturn(true);
mAdapter = new LocalDisplayAdapter(mMockedSyncRoot, mMockedContext, mHandler,
@@ -211,7 +225,15 @@
when(mMockedResources.getIntArray(
com.android.internal.R.array.config_autoBrightnessLcdBacklightValues))
.thenReturn(new int[]{});
+
+ when(mMockedColorDisplayServiceInternal.fetchEvenDimmerSpline(3)).thenReturn(
+ new Spline.LinearSpline(
+ new float[]{2f, 3.0f, 500f, 2000f},
+ new float[]{100, 0, 0, 0}));
+ when(mMockDisplayDeviceConfig.isEvenDimmerAvailable()).thenReturn(true);
+
doReturn(true).when(mFlags).isDisplayOffloadEnabled();
+ doReturn(true).when(mFlags).isEvenDimmerEnabled();
initDisplayOffloadSession();
}
@@ -222,6 +244,122 @@
}
}
+ @Test
+ public void testEvenDimmer() throws InterruptedException {
+ // Set up
+ FakeDisplay display = new FakeDisplay(PORT_A);
+ setUpDisplay(display);
+ updateAvailableDisplays();
+ mAdapter.registerLocked();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+ DisplayDevice displayDevice = mListener.addedDisplays.get(0);
+
+ // brightness|backlight| nits | strength
+ // 0.5 | 0.45 | 600 | 0 // initial setup value
+ // 0.4 | 0.35 | 500 | 0 // normal range value
+ // 0.31 | 0.2 | 3 | 0 // transition point
+ // 0.16 | 0.125 | 2.5 | 50 // mid point of even dimmer
+ // 0.1 | 0.05 | 2 | 100 // bottom of even dimmer range
+ // 0.05 | 0.01 | 1 | 100+ // beyond strength=100 range (should still return 100)
+ when(mMockDisplayDeviceConfig.getEvenDimmerTransitionPoint()).thenReturn(0.31f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.5f)).thenReturn(0.45f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.4f)).thenReturn(0.35f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.31f)).thenReturn(0.2f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.16f)).thenReturn(0.125f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.1f)).thenReturn(0.05f);
+ when(mMockDisplayDeviceConfig.getBacklightFromBrightness(0.05f)).thenReturn(0.01f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.45f)).thenReturn(600f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.35f)).thenReturn(500f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.2f)).thenReturn(3f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.125f)).thenReturn(2.5f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.05f)).thenReturn(2f);
+ when(mMockDisplayDeviceConfig.getNitsFromBacklight(0.01f)).thenReturn(1f);
+
+ // initialise brightness to 0.5
+ Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON,
+ 0.5f, 0.5f, null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ verify(mSurfaceControlProxy).setDisplayPowerMode(any(), anyInt());
+ verify(mMockBacklightAdapter).setBacklight(anyFloat(), anyFloat(), anyFloat(), anyFloat());
+ verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(false), eq(0));
+ verify(mMockedColorDisplayServiceInternal).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up normal brightness range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.4f, 0.4f,
+ null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify normal brightness range
+ verify(mMockBacklightAdapter).setBacklight(0.35f, 500f, 0.35f, 500f);
+ verify(mMockedColorDisplayServiceInternal,
+ times(1)) // no more, since the strength is the same
+ .applyEvenDimmerColorChanges(eq(false), eq(0));
+ verify(mMockedColorDisplayServiceInternal, times(2)).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up even dimmer edge range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.31f,
+ 0.31f, null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify even dimmer edge range
+ verify(mMockBacklightAdapter).setBacklight(0.2f, 3f, 0.2f, 3f);
+ // verify no more times, since the strength and enabled-ness is the same
+ verify(mMockedColorDisplayServiceInternal, times(1)).applyEvenDimmerColorChanges(eq(false),
+ eq(0));
+ verify(mMockedColorDisplayServiceInternal, times(3)).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up mid point of even dimmer range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.16f,
+ 0.16f, null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify within even dimmer range
+ verify(mMockBacklightAdapter).setBacklight(0.125f, 2.5f, 0.125f, 2.5f);
+ verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(50));
+ verify(mMockedColorDisplayServiceInternal, times(4)).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up within even dimmer range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.1f, 0.1f,
+ null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify within even dimmer range
+ verify(mMockBacklightAdapter).setBacklight(0.05f, 2f, 0.05f, 2f);
+ verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(100));
+ verify(mMockedColorDisplayServiceInternal, times(5)).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up below even dimmer range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.05f,
+ 0.05f, null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify within even dimmer range
+ verify(mMockBacklightAdapter).setBacklight(0.01f, 1f, 0.01f, 1f);
+ // ensure no greater than 100 strength is returned, therefore not called again.
+ verify(mMockedColorDisplayServiceInternal).applyEvenDimmerColorChanges(eq(true), eq(100));
+ verify(mMockedColorDisplayServiceInternal, times(6)).fetchEvenDimmerSpline(eq(3.0f));
+
+ // set up return to normal range
+ changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0.4f, 0.4f,
+ null);
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ changeStateRunnable.run();
+ waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+ // verify return to normal range
+ verify(mMockBacklightAdapter, times(2)).setBacklight(0.35f, 500f, 0.35f, 500f);
+ verify(mMockedColorDisplayServiceInternal, times(2)).applyEvenDimmerColorChanges(eq(false),
+ anyInt());
+ verify(mMockedColorDisplayServiceInternal, times(7)).fetchEvenDimmerSpline(eq(3.0f));
+ }
+
/**
* Confirm that display is marked as private when it is listed in
* com.android.internal.R.array.config_localPrivateDisplayPorts.
@@ -1461,15 +1599,16 @@
return mSurfaceControlProxy;
}
- // Instead of using DisplayDeviceConfig.create(context, physicalDisplayId, isFirstDisplay)
- // we should use DisplayDeviceConfig.create(context, isFirstDisplay) for the test to ensure
- // that real device DisplayDeviceConfig is not loaded for FakeDisplay and we are getting
- // consistent behaviour. Please also note that context passed to this method, is
- // mMockContext and values will be loaded from mMockResources.
@Override
public DisplayDeviceConfig createDisplayDeviceConfig(Context context,
long physicalDisplayId, boolean isFirstDisplay, DisplayManagerFlags flags) {
- return DisplayDeviceConfig.create(context, isFirstDisplay, flags);
+ return mMockDisplayDeviceConfig;
+ }
+
+ @Override
+ public BacklightAdapter getBacklightAdapter(IBinder displayToken, boolean isFirstDisplay,
+ LocalDisplayAdapter.SurfaceControlProxy surfaceControlProxy) {
+ return mMockBacklightAdapter;
}
}
diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
index fc4d8d8..0702926 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
@@ -16,6 +16,9 @@
package com.android.server.power;
+import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
+import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
+
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
@@ -31,11 +34,13 @@
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
+import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.res.Resources;
import android.hardware.SensorManager;
import android.hardware.display.AmbientDisplayConfiguration;
+import android.hardware.display.DisplayManagerInternal;
import android.os.BatteryStats;
import android.os.Handler;
import android.os.IWakeLockCallback;
@@ -48,11 +53,18 @@
import android.os.test.TestLooper;
import android.provider.Settings;
import android.testing.TestableContext;
+import android.util.IntArray;
+import android.util.SparseBooleanArray;
+import android.view.Display;
+import android.view.DisplayAddress;
+import android.view.DisplayInfo;
import androidx.test.InstrumentationRegistry;
import com.android.internal.app.IBatteryStats;
import com.android.server.LocalServices;
+import com.android.server.input.InputManagerInternal;
+import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.power.batterysaver.BatterySaverStateMachine;
import com.android.server.power.feature.PowerManagerFlags;
@@ -71,6 +83,8 @@
public class NotifierTest {
private static final String SYSTEM_PROPERTY_QUIESCENT = "ro.boot.quiescent";
private static final int USER_ID = 0;
+ private static final int DISPLAY_PORT = 0xFF;
+ private static final long DISPLAY_MODEL = 0xEEEEEEEEL;
@Mock private BatterySaverStateMachine mBatterySaverStateMachineMock;
@Mock private PowerManagerService.NativeWrapper mNativeWrapperMock;
@@ -81,10 +95,16 @@
@Mock private InattentiveSleepWarningController mInattentiveSleepWarningControllerMock;
@Mock private Vibrator mVibrator;
@Mock private StatusBarManagerInternal mStatusBarManagerInternal;
+ @Mock private InputManagerInternal mInputManagerInternal;
+ @Mock private InputMethodManagerInternal mInputMethodManagerInternal;
+ @Mock private DisplayManagerInternal mDisplayManagerInternal;
+ @Mock private ActivityManagerInternal mActivityManagerInternal;
@Mock private WakeLockLog mWakeLockLog;
@Mock private IBatteryStats mBatteryStats;
+ @Mock private WindowManagerPolicy mPolicy;
+
@Mock private PowerManagerFlags mPowerManagerFlags;
@Mock private AppOpsManager mAppOpsManager;
@@ -96,6 +116,8 @@
private FakeExecutor mTestExecutor = new FakeExecutor();
private Notifier mNotifier;
+ private DisplayInfo mDefaultDisplayInfo = new DisplayInfo();
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -103,11 +125,25 @@
LocalServices.removeServiceForTest(StatusBarManagerInternal.class);
LocalServices.addService(StatusBarManagerInternal.class, mStatusBarManagerInternal);
+ LocalServices.removeServiceForTest(InputManagerInternal.class);
+ LocalServices.addService(InputManagerInternal.class, mInputManagerInternal);
+ LocalServices.removeServiceForTest(InputMethodManagerInternal.class);
+ LocalServices.addService(InputMethodManagerInternal.class, mInputMethodManagerInternal);
+
+ LocalServices.removeServiceForTest(ActivityManagerInternal.class);
+ LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInternal);
+
+ mDefaultDisplayInfo.address = DisplayAddress.fromPortAndModel(DISPLAY_PORT, DISPLAY_MODEL);
+ LocalServices.removeServiceForTest(DisplayManagerInternal.class);
+ LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternal);
+
mContextSpy = spy(new TestableContext(InstrumentationRegistry.getContext()));
mResourcesSpy = spy(mContextSpy.getResources());
when(mContextSpy.getResources()).thenReturn(mResourcesSpy);
when(mSystemPropertiesMock.get(eq(SYSTEM_PROPERTY_QUIESCENT), anyString())).thenReturn("");
when(mContextSpy.getSystemService(Vibrator.class)).thenReturn(mVibrator);
+ when(mDisplayManagerInternal.getDisplayInfo(Display.DEFAULT_DISPLAY)).thenReturn(
+ mDefaultDisplayInfo);
mService = new PowerManagerService(mContextSpy, mInjector);
}
@@ -232,6 +268,32 @@
}
@Test
+ public void testOnGlobalWakefulnessChangeStarted() throws Exception {
+ createNotifier();
+ // GIVEN system is currently non-interactive
+ when(mPowerManagerFlags.isPerDisplayWakeByTouchEnabled()).thenReturn(false);
+ final int displayId1 = 101;
+ final int displayId2 = 102;
+ final int[] displayIds = new int[]{displayId1, displayId2};
+ when(mDisplayManagerInternal.getDisplayIds()).thenReturn(IntArray.wrap(displayIds));
+ mNotifier.onGlobalWakefulnessChangeStarted(WAKEFULNESS_ASLEEP,
+ PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON, /* eventTime= */ 1000);
+ mTestLooper.dispatchAll();
+
+ // WHEN a global wakefulness change to interactive starts
+ mNotifier.onGlobalWakefulnessChangeStarted(WAKEFULNESS_AWAKE,
+ PowerManager.WAKE_REASON_TAP, /* eventTime= */ 2000);
+ mTestLooper.dispatchAll();
+
+ // THEN input is notified of all displays being interactive
+ final SparseBooleanArray expectedDisplayInteractivities = new SparseBooleanArray();
+ expectedDisplayInteractivities.put(displayId1, true);
+ expectedDisplayInteractivities.put(displayId2, true);
+ verify(mInputManagerInternal).setDisplayInteractivities(expectedDisplayInteractivities);
+ verify(mInputMethodManagerInternal).setInteractive(/* interactive= */ true);
+ }
+
+ @Test
public void testOnWakeLockListener_RemoteException_NoRethrow() throws RemoteException {
when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true);
createNotifier();
@@ -551,7 +613,7 @@
mContextSpy,
mBatteryStats,
mInjector.createSuspendBlocker(mService, "testBlocker"),
- null,
+ mPolicy,
null,
null,
mTestExecutor, mPowerManagerFlags, injector);
diff --git a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
index fd55428..c3df078 100644
--- a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
+++ b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml
@@ -32,4 +32,7 @@
<device name="test.sensor.device">
<subsystem>Sensor</subsystem>
</device>
+ <device name="test.bluetooth.device">
+ <subsystem>Bluetooth</subsystem>
+ </device>
</irq-device-map>
\ No newline at end of file
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
index 1d20538..c037f97 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
@@ -927,7 +927,7 @@
assertThat(mPowerStatsStore.getTableOfContents()).isEmpty();
mBatteryStatsImpl.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider,
- mPowerStatsStore);
+ mPowerStatsStore, /* accumulateBatteryUsageStats */ false);
synchronized (mBatteryStatsImpl) {
mBatteryStatsImpl.noteFlashlightOnLocked(42, mMockClock.realtime, mMockClock.uptime);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index fde84e9..0e60156 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -419,7 +419,8 @@
mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
- batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore);
+ batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+ /* accumulateBatteryUsageStats */ false);
synchronized (batteryStats) {
batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
}
@@ -505,6 +506,102 @@
.of(180.0);
}
+ @Test
+ public void accumulateBatteryUsageStats() {
+ BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
+
+ setTime(5 * MINUTE_IN_MS);
+
+ // Capture the session start timestamp
+ synchronized (batteryStats) {
+ batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+ }
+
+ PowerStatsStore powerStatsStore = new PowerStatsStore(
+ new File(mStatsRule.getHistoryDir(), getClass().getSimpleName()),
+ mStatsRule.getHandler());
+ powerStatsStore.reset();
+
+ BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext,
+ mock(PowerAttributor.class), mStatsRule.getPowerProfile(),
+ mStatsRule.getCpuScalingPolicies(), powerStatsStore, mMockClock);
+
+ batteryStats.saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+ /* accumulateBatteryUsageStats */ true);
+
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOnLocked(APP_UID,
+ 10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
+ }
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOffLocked(APP_UID,
+ 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
+ }
+
+ synchronized (batteryStats) {
+ batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+ }
+
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOnLocked(APP_UID,
+ 30 * MINUTE_IN_MS, 30 * MINUTE_IN_MS);
+ }
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOffLocked(APP_UID,
+ 50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
+ }
+ setTime(55 * MINUTE_IN_MS);
+ synchronized (batteryStats) {
+ batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND);
+ }
+
+ // This section has not been saved yet, but should be added to the accumulated totals
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOnLocked(APP_UID,
+ 80 * MINUTE_IN_MS, 80 * MINUTE_IN_MS);
+ }
+ synchronized (batteryStats) {
+ batteryStats.noteFlashlightOffLocked(APP_UID,
+ 110 * MINUTE_IN_MS, 110 * MINUTE_IN_MS);
+ }
+ setTime(115 * MINUTE_IN_MS);
+
+ // Await completion
+ ConditionVariable done = new ConditionVariable();
+ mStatsRule.getHandler().post(done::open);
+ done.block();
+
+ BatteryUsageStats stats = provider.getBatteryUsageStats(batteryStats,
+ new BatteryUsageStatsQuery.Builder().accumulated().build());
+
+ assertThat(stats.getStatsStartTimestamp()).isEqualTo(5 * MINUTE_IN_MS);
+ assertThat(stats.getStatsEndTimestamp()).isEqualTo(115 * MINUTE_IN_MS);
+
+ // Section 1 (saved): 20 - 10 = 10
+ // Section 2 (saved): 50 - 30 = 20
+ // Section 3 (fresh): 110 - 80 = 30
+ // Total: 10 + 20 + 30 = 60
+ assertThat(stats.getAggregateBatteryConsumer(
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ .getConsumedPower(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+ .isWithin(0.0001)
+ .of(360.0); // 360 mA * 1.0 hour
+ assertThat(stats.getAggregateBatteryConsumer(
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+ .isEqualTo(60 * MINUTE_IN_MS);
+
+ final UidBatteryConsumer uidBatteryConsumer = stats.getUidBatteryConsumers().stream()
+ .filter(uid -> uid.getUid() == APP_UID).findFirst().get();
+ assertThat(uidBatteryConsumer
+ .getConsumedPower(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+ .isWithin(0.1)
+ .of(360.0);
+ assertThat(uidBatteryConsumer
+ .getUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_FLASHLIGHT))
+ .isEqualTo(60 * MINUTE_IN_MS);
+ }
+
private void setTime(long timeMs) {
mMockClock.currentTime = timeMs;
mMockClock.realtime = timeMs;
@@ -550,7 +647,8 @@
return null;
}).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any());
- mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore);
+ mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore,
+ /* accumulateBatteryUsageStats */ false);
// Make an incompatible change of supported energy components. This will trigger
// a BatteryStats reset, which will generate a snapshot of battery stats.
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
index 0dc836b..fe4d971 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java
@@ -17,6 +17,7 @@
package com.android.server.power.stats.wakeups;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM;
+import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR;
import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER;
@@ -52,6 +53,7 @@
private static final String KERNEL_REASON_SOUND_TRIGGER_IRQ = "129 test.sound_trigger.device";
private static final String KERNEL_REASON_SENSOR_IRQ = "15 test.sensor.device";
private static final String KERNEL_REASON_CELLULAR_DATA_IRQ = "18 test.cellular_data.device";
+ private static final String KERNEL_REASON_BLUETOOTH_IRQ = "19 test.bluetooth.device";
private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device";
private static final String KERNEL_REASON_UNKNOWN_FORMAT = "free-form-reason test.alarm.device";
private static final String KERNEL_REASON_ALARM_ABNORMAL = "-1 test.alarm.device";
@@ -62,12 +64,14 @@
private static final int TEST_UID_3 = 92261423;
private static final int TEST_UID_4 = 56926423;
private static final int TEST_UID_5 = 76421423;
+ private static final int TEST_UID_6 = 62345353;
private static final int TEST_PROC_STATE_1 = 72331;
private static final int TEST_PROC_STATE_2 = 792351;
private static final int TEST_PROC_STATE_3 = 138831;
private static final int TEST_PROC_STATE_4 = 23231;
private static final int TEST_PROC_STATE_5 = 42;
+ private static final int TEST_PROC_STATE_6 = 129942;
private static final Context sContext = InstrumentationRegistry.getTargetContext();
private final Handler mHandler = Mockito.mock(Handler.class);
@@ -79,6 +83,7 @@
obj.mUidProcStates.put(TEST_UID_3, TEST_PROC_STATE_3);
obj.mUidProcStates.put(TEST_UID_4, TEST_PROC_STATE_4);
obj.mUidProcStates.put(TEST_UID_5, TEST_PROC_STATE_5);
+ obj.mUidProcStates.put(TEST_UID_6, TEST_PROC_STATE_6);
}
@Test
@@ -118,6 +123,7 @@
CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER,
CPU_WAKEUP_SUBSYSTEM_SENSOR,
CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA,
+ CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
};
final String[] kernelReasons = new String[] {
@@ -126,10 +132,11 @@
KERNEL_REASON_SOUND_TRIGGER_IRQ,
KERNEL_REASON_SENSOR_IRQ,
KERNEL_REASON_CELLULAR_DATA_IRQ,
+ KERNEL_REASON_BLUETOOTH_IRQ,
};
final int[] uids = new int[] {
- TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5
+ TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5, TEST_UID_6
};
final int[] procStates = new int[] {
@@ -137,7 +144,8 @@
TEST_PROC_STATE_3,
TEST_PROC_STATE_4,
TEST_PROC_STATE_1,
- TEST_PROC_STATE_5
+ TEST_PROC_STATE_5,
+ TEST_PROC_STATE_6
};
final int total = subsystems.length;
@@ -285,6 +293,40 @@
}
@Test
+ public void bluetoothIrqAttributionSolo() {
+ final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
+ final long wakeupTime = 1236121;
+
+ populateDefaultProcStates(obj);
+
+ obj.noteWakeupTimeAndReason(wakeupTime, 1, KERNEL_REASON_BLUETOOTH_IRQ);
+
+ // Outside the window, so should be ignored.
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+ wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1);
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+ wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2);
+ // Should be attributed
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 5, TEST_UID_3,
+ TEST_UID_5);
+
+ final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+ assertThat(attribution).isNotNull();
+ assertThat(attribution.size()).isEqualTo(1);
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue();
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+ TEST_UID_1)).isLessThan(0);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+ TEST_UID_2)).isLessThan(0);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo(
+ TEST_PROC_STATE_3);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey(
+ TEST_UID_4)).isLessThan(0);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo(
+ TEST_PROC_STATE_5);
+ }
+
+ @Test
public void alarmAndWifiIrqAttribution() {
final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
final long wakeupTime = 92123210;
@@ -400,6 +442,47 @@
}
@Test
+ public void unknownAndBluetoothAttribution() {
+ final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
+ final long wakeupTime = 92123520;
+
+ populateDefaultProcStates(obj);
+
+ obj.noteWakeupTimeAndReason(wakeupTime, 24,
+ KERNEL_REASON_UNKNOWN_IRQ + ":" + KERNEL_REASON_BLUETOOTH_IRQ);
+
+ // Bluetooth activity
+ // Outside the window, so should be ignored.
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH,
+ wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_4);
+ // Should be attributed
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 2, TEST_UID_1);
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime - 1, TEST_UID_3,
+ TEST_UID_5);
+
+ // Unrelated, should be ignored.
+ obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3);
+
+ final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime);
+ assertThat(attribution).isNotNull();
+ assertThat(attribution.size()).isEqualTo(2);
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue();
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_1)).isEqualTo(
+ TEST_PROC_STATE_1);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)
+ .indexOfKey(TEST_UID_2)).isLessThan(0);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo(
+ TEST_PROC_STATE_3);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)
+ .indexOfKey(TEST_UID_4)).isLessThan(0);
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo(
+ TEST_PROC_STATE_5);
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue();
+ assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isNull();
+ assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isFalse();
+ }
+
+ @Test
public void unknownFormatWakeupIgnored() {
final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler);
final long wakeupTime = 72123210;
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index bbe0755..cbe6700 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -52,8 +52,10 @@
"services.credentials",
"services.devicepolicy",
"services.flags",
+ "com.android.server.flags.services-aconfig-java",
"services.net",
"services.people",
+ "services.supervision",
"services.usage",
"service-permission.stubs.system_server",
"guava",
@@ -80,6 +82,7 @@
// TODO: remove once Android migrates to JUnit 4.12,
// which provides assertThrows
"testng",
+ "flag-junit",
"junit",
"junit-params",
"ActivityContext",
diff --git a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
index ec78bce..c18faef 100644
--- a/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/PinnerServiceTest.java
@@ -31,6 +31,9 @@
import android.os.Binder;
import android.os.Handler;
import android.os.Looper;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.DeviceConfig;
import android.provider.DeviceConfigInterface;
import android.testing.TestableContext;
@@ -43,6 +46,9 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.server.flags.Flags;
+import com.android.server.pinner.PinnedFile;
+import com.android.server.pinner.PinnerService;
import com.android.server.testutils.FakeDeviceConfigInterface;
import com.android.server.wm.ActivityTaskManagerInternal;
@@ -73,15 +79,18 @@
private static final long WAIT_FOR_PINNER_TIMEOUT = TimeUnit.SECONDS.toMillis(2);
+ private static final int MEMORY_PERCENTAGE_FOR_QUOTA = 10;
+
@Rule
public TestableContext mContext =
new TestableContext(InstrumentationRegistry.getContext(), null);
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private final ArraySet<String> mUpdatedPackages = new ArraySet<>();
private ResolveInfo mHomePackageResolveInfo;
private FakeDeviceConfigInterface mFakeDeviceConfigInterface;
private PinnerService.Injector mInjector;
-
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -114,6 +123,8 @@
resources.addOverride(com.android.internal.R.bool.config_pinnerCameraApp, false);
resources.addOverride(com.android.internal.R.integer.config_pinnerHomePinBytes, 0);
resources.addOverride(com.android.internal.R.bool.config_pinnerAssistantApp, false);
+ resources.addOverride(com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage,
+ MEMORY_PERCENTAGE_FOR_QUOTA);
mFakeDeviceConfigInterface = new FakeDeviceConfigInterface();
setDeviceConfigPinnedAnonSize(0);
@@ -138,10 +149,9 @@
}
@Override
- protected PinnerService.PinnedFile pinFileInternal(String fileToPin,
- int maxBytesToPin, boolean attemptPinIntrospection) {
- return new PinnerService.PinnedFile(-1,
- maxBytesToPin, fileToPin, maxBytesToPin);
+ protected PinnedFile pinFileInternal(PinnerService service, String fileToPin,
+ long maxBytesToPin, boolean attemptPinIntrospection) {
+ return new PinnedFile(-1, maxBytesToPin, fileToPin, maxBytesToPin);
}
};
}
@@ -167,6 +177,12 @@
unpinAnonRegionMethod.invoke(pinnerService);
}
+ private long getGlobalPinQuota(PinnerService service) throws Exception {
+ Method getQuotaMethod = PinnerService.class.getDeclaredMethod("getAvailableGlobalQuota");
+ getQuotaMethod.setAccessible(true);
+ return (long) getQuotaMethod.invoke(service);
+ }
+
private void waitForPinnerService(PinnerService pinnerService)
throws NoSuchFieldException, IllegalAccessException {
// There's no notification/callback when pinning finished
@@ -315,15 +331,121 @@
PinnerService pinnerService = new PinnerService(mContext, mInjector);
pinnerService.onStart();
- pinnerService.pinFile("test_file", 4096, null, "my_group");
+ pinnerService.pinFile("test_file", 4096, null, "my_group", false);
- assertThat(getPinnedSize(pinnerService)).isGreaterThan(0);
- assertThat(getTotalPinnedFiles(pinnerService)).isGreaterThan(0);
+ assertThat(getPinnedSize(pinnerService)).isEqualTo(4096);
+ assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(1);
unpinAll(pinnerService);
}
@Test
+ @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testPinAllQuota() throws Exception {
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+
+ long quota = getGlobalPinQuota(pinnerService);
+
+ pinnerService.pinFile("test_file", Long.MAX_VALUE, null, "my_group", false);
+
+ assertThat(getPinnedSize(pinnerService)).isEqualTo(quota);
+
+ unpinAll(pinnerService);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testGlobalPinQuotaAsDevicePercentage() throws Exception {
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+ long origQuota = getGlobalPinQuota(pinnerService);
+
+ long totalMem = android.os.Process.getTotalMemory();
+
+ // Verify that pin quota is the set percentage of device total memory
+ assertThat(origQuota).isEqualTo((totalMem * MEMORY_PERCENTAGE_FOR_QUOTA) / 100);
+
+ pinnerService.pinFile("test_file", 4096, null, "my_group", false);
+ assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota - 4096);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testGlobalPinWhenNoQuota() throws Exception {
+ TestableResources resources = mContext.getOrCreateTestableResources();
+ resources.addOverride(
+ com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0);
+
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+
+ // Verify that pin quota is zero
+ assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0);
+
+ pinnerService.pinFile("test_file", 4096, null, "my_group", false);
+ assertThat(getTotalPinnedFiles(pinnerService)).isEqualTo(0);
+ }
+
+ /**
+ * This test is temporary, it should be cleaned up when removing the pin_global_quota bugfix
+ * flag.
+ */
+ @Test
+ @DisableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testGlobalQuotaDisabled() throws Exception {
+ TestableResources resources = mContext.getOrCreateTestableResources();
+ resources.addOverride(
+ com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, 0);
+
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+
+ // The quota parameter exists but it should have no effect on pinning
+ long quota = getGlobalPinQuota(pinnerService);
+
+ pinnerService.pinFile("test_file", quota + 1, null, "my_group", false);
+
+ // Verify that we can pin past the quota as it is disabled
+ assertThat(getPinnedSize(pinnerService)).isEqualTo(quota + 1);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testUnpinReleasesQuota() throws Exception {
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+ long origQuota = getGlobalPinQuota(pinnerService);
+
+ // Verify that pin quota exists and is non zero.
+ assertThat(getGlobalPinQuota(pinnerService)).isGreaterThan(0);
+
+ pinnerService.pinFile("test_file", origQuota, null, "my_group", false);
+
+ // Make sure all the quota was consumed
+ assertThat(getPinnedSize(pinnerService)).isEqualTo(origQuota);
+
+ // Unpin the file and verify that the quota has been released.
+ pinnerService.unpinFile("test_file");
+ assertThat(getPinnedSize(pinnerService)).isEqualTo(0);
+ assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(origQuota);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_PIN_GLOBAL_QUOTA)
+ public void testGlobalPinQuotaNegative() throws Exception {
+ TestableResources resources = mContext.getOrCreateTestableResources();
+ resources.addOverride(
+ com.android.internal.R.integer.config_pinnerMaxPinnedMemoryPercentage, -10);
+
+ PinnerService pinnerService = new PinnerService(mContext, mInjector);
+ pinnerService.onStart();
+
+ // Verify that pin quota is zero
+ assertThat(getGlobalPinQuota(pinnerService)).isEqualTo(0);
+ }
+
+ @Test
public void testPinAnonRegion() throws Exception {
setDeviceConfigPinnedAnonSize(32768);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
index 6e6d5a8..8dfd54f 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AbstractAccessibilityServiceConnectionTest.java
@@ -174,8 +174,8 @@
@Mock private AccessibilityTrace mMockA11yTrace;
@Mock private WindowManagerInternal mMockWindowManagerInternal;
@Mock private SystemActionPerformer mMockSystemActionPerformer;
- @Mock private IBinder mMockService;
- @Mock private IAccessibilityServiceClient mMockServiceInterface;
+ @Mock private IBinder mMockClientBinder;
+ @Mock private IAccessibilityServiceClient mMockClient;
@Mock private KeyEventDispatcher mMockKeyEventDispatcher;
@Mock private IAccessibilityInteractionConnection mMockIA11yInteractionConnection;
@Mock private IAccessibilityInteractionConnectionCallback mMockCallback;
@@ -247,9 +247,9 @@
mSpyServiceInfo, SERVICE_ID, mHandler, new Object(), mMockSecurityPolicy,
mMockSystemSupport, mMockA11yTrace, mMockWindowManagerInternal,
mMockSystemActionPerformer, mMockA11yWindowManager);
- // Assume that the service is connected
- mServiceConnection.mService = mMockService;
- mServiceConnection.mServiceInterface = mMockServiceInterface;
+ // Assume that the client is connected
+ mServiceConnection.mClientBinder = mMockClientBinder;
+ mServiceConnection.mClient = mMockClient;
// Update security policy for this service
when(mMockSecurityPolicy.checkAccessibilityAccess(mServiceConnection)).thenReturn(true);
@@ -273,7 +273,7 @@
final KeyEvent mockKeyEvent = mock(KeyEvent.class);
mServiceConnection.onKeyEvent(mockKeyEvent, sequenceNumber);
- verify(mMockServiceInterface).onKeyEvent(mockKeyEvent, sequenceNumber);
+ verify(mMockClient).onKeyEvent(mockKeyEvent, sequenceNumber);
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 566feb7..7481fc8 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -63,8 +63,11 @@
import android.Manifest;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceClient;
+import android.annotation.NonNull;
import android.app.PendingIntent;
import android.app.RemoteAction;
+import android.app.admin.DevicePolicyManager;
+import android.app.ecm.EnhancedConfirmationManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -212,6 +215,7 @@
@Mock private FullScreenMagnificationController mMockFullScreenMagnificationController;
@Mock private ProxyManager mProxyManager;
@Mock private StatusBarManagerInternal mStatusBarManagerInternal;
+ @Mock private DevicePolicyManager mDevicePolicyManager;
@Spy private IUserInitializationCompleteCallback mUserInitializationCompleteCallback;
@Captor private ArgumentCaptor<Intent> mIntentArgumentCaptor;
private IAccessibilityManager mA11yManagerServiceOnDevice;
@@ -241,6 +245,7 @@
UserManagerInternal.class, mMockUserManagerInternal);
LocalServices.addService(StatusBarManagerInternal.class, mStatusBarManagerInternal);
mInputFilter = mock(FakeInputFilter.class);
+ mTestableContext.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManager);
when(mMockMagnificationController.getMagnificationConnectionManager()).thenReturn(
mMockMagnificationConnectionManager);
@@ -2160,6 +2165,24 @@
.isEqualTo(SOFTWARE);
}
+ @Test
+ @EnableFlags({android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED,
+ android.security.Flags.FLAG_EXTEND_ECM_TO_ALL_SETTINGS})
+ public void isAccessibilityTargetAllowed_nonSystemUserId_useEcmWithNonSystemUserId() {
+ String fakePackageName = "FAKE_PACKAGE_NAME";
+ int uid = 0; // uid is not used in the actual implementation when flags are on
+ int userId = mTestableContext.getUserId() + 1234;
+ when(mDevicePolicyManager.getPermittedAccessibilityServices(userId)).thenReturn(
+ List.of(fakePackageName));
+ Context mockUserContext = mock(Context.class);
+ mTestableContext.addMockUserContext(userId, mockUserContext);
+
+ mA11yms.isAccessibilityTargetAllowed(fakePackageName, uid, userId);
+
+ verify(mockUserContext).getSystemService(EnhancedConfirmationManager.class);
+ }
+
+
private Set<String> readStringsFromSetting(String setting) {
final Set<String> result = new ArraySet<>();
mA11yms.readColonDelimitedSettingToSet(
@@ -2280,6 +2303,7 @@
private final Context mMockContext;
private final Map<String, List<BroadcastReceiver>> mBroadcastReceivers = new ArrayMap<>();
+ private ArrayMap<Integer, Context> mMockUserContexts = new ArrayMap<>();
A11yTestableContext(Context base) {
super(base);
@@ -2317,6 +2341,19 @@
return mMockContext;
}
+ public void addMockUserContext(int userId, Context context) {
+ mMockUserContexts.put(userId, context);
+ }
+
+ @Override
+ @NonNull
+ public Context createContextAsUser(UserHandle user, int flags) {
+ if (mMockUserContexts.containsKey(user.getIdentifier())) {
+ return mMockUserContexts.get(user.getIdentifier());
+ }
+ return super.createContextAsUser(user, flags);
+ }
+
Map<String, List<BroadcastReceiver>> getBroadcastReceivers() {
return mBroadcastReceivers;
}
diff --git a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
index b9ce8ad..0c92abc 100644
--- a/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accounts/AccountManagerServiceTest.java
@@ -1163,6 +1163,16 @@
verify(mMockAccountManagerResponse).onResult(mBundleCaptor.capture());
Bundle result = mBundleCaptor.getValue();
+ Bundle sessionBundle = result.getBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE);
+ assertNotNull(sessionBundle);
+ // Assert that session bundle is decrypted and hence data is visible.
+ assertEquals(AccountManagerServiceTestFixtures.SESSION_DATA_VALUE_1,
+ sessionBundle.getString(AccountManagerServiceTestFixtures.SESSION_DATA_NAME_1));
+ // Assert finishSessionAsUser added calling uid and pid into the sessionBundle
+ assertTrue(sessionBundle.containsKey(AccountManager.KEY_CALLER_UID));
+ assertTrue(sessionBundle.containsKey(AccountManager.KEY_CALLER_PID));
+ assertEquals(sessionBundle.getString(
+ AccountManager.KEY_ANDROID_PACKAGE_NAME), "APCT.package");
// Verify response data
assertNull(result.getString(AccountManager.KEY_AUTHTOKEN, null));
@@ -2111,6 +2121,12 @@
result.getString(AccountManager.KEY_ACCOUNT_NAME));
assertEquals(AccountManagerServiceTestFixtures.ACCOUNT_TYPE_1,
result.getString(AccountManager.KEY_ACCOUNT_TYPE));
+
+ Bundle optionBundle = result.getParcelable(
+ AccountManagerServiceTestFixtures.KEY_OPTIONS_BUNDLE);
+ // Assert addAccountAsUser added calling uid and pid into the option bundle
+ assertTrue(optionBundle.containsKey(AccountManager.KEY_CALLER_UID));
+ assertTrue(optionBundle.containsKey(AccountManager.KEY_CALLER_PID));
}
@SmallTest
@@ -3441,52 +3457,6 @@
+ (readTotalTime.doubleValue() / readerCount / loopSize));
}
- @SmallTest
- public void testSanitizeBundle_expectedFields() throws Exception {
- Bundle bundle = new Bundle();
- bundle.putString(AccountManager.KEY_ACCOUNT_NAME, "name");
- bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, "type");
- bundle.putString(AccountManager.KEY_AUTHTOKEN, "token");
- bundle.putString(AccountManager.KEY_AUTH_TOKEN_LABEL, "label");
- bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "error message");
- bundle.putString(AccountManager.KEY_PASSWORD, "password");
- bundle.putString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN, "status");
-
- bundle.putLong(AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY, 123L);
- bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
- bundle.putInt(AccountManager.KEY_ERROR_CODE, 456);
-
- Bundle sanitizedBundle = AccountManagerService.sanitizeBundle(bundle);
- assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_NAME), "name");
- assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_TYPE), "type");
- assertEquals(sanitizedBundle.getString(AccountManager.KEY_AUTHTOKEN), "token");
- assertEquals(sanitizedBundle.getString(AccountManager.KEY_AUTH_TOKEN_LABEL), "label");
- assertEquals(sanitizedBundle.getString(AccountManager.KEY_ERROR_MESSAGE), "error message");
- assertEquals(sanitizedBundle.getString(AccountManager.KEY_PASSWORD), "password");
- assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN), "status");
-
- assertEquals(sanitizedBundle.getLong(
- AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY, 0), 123L);
- assertEquals(sanitizedBundle.getBoolean(AccountManager.KEY_BOOLEAN_RESULT, false), true);
- assertEquals(sanitizedBundle.getInt(AccountManager.KEY_ERROR_CODE, 0), 456);
- }
-
- @SmallTest
- public void testSanitizeBundle_filtersUnexpectedFields() throws Exception {
- Bundle bundle = new Bundle();
- bundle.putString(AccountManager.KEY_ACCOUNT_NAME, "name");
- bundle.putString("unknown_key", "value");
- Bundle sessionBundle = new Bundle();
- bundle.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle);
-
- Bundle sanitizedBundle = AccountManagerService.sanitizeBundle(bundle);
-
- assertEquals(sanitizedBundle.getString(AccountManager.KEY_ACCOUNT_NAME), "name");
- assertFalse(sanitizedBundle.containsKey("unknown_key"));
- // It is a valid response from Authenticator which will be accessed using original Bundle
- assertFalse(sanitizedBundle.containsKey(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE));
- }
-
private void waitForCyclicBarrier(CyclicBarrier cyclicBarrier) {
try {
cyclicBarrier.await(LATCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 4645156..3e2949d6 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -16,7 +16,9 @@
package com.android.server.audio;
+import static com.android.media.audio.Flags.FLAG_ABS_VOLUME_INDEX_FIX;
import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
+import static com.android.media.audio.Flags.absVolumeIndexFix;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
@@ -116,7 +118,7 @@
}
@Test
- @RequiresFlagsDisabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
+ @RequiresFlagsDisabled({FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME, FLAG_ABS_VOLUME_INDEX_FIX})
public void configurablePreScaleAbsoluteVolume_checkIndex() throws Exception {
AudioManager am = mContext.getSystemService(AudioManager.class);
final int minIndex = am.getStreamMinVolume(AudioManager.STREAM_MUSIC);
@@ -177,6 +179,7 @@
final AudioDeviceAttributes bleDevice = new AudioDeviceAttributes(
/*native type*/ AudioSystem.DEVICE_OUT_BLE_HEADSET, /*address*/ "bla");
final int maxPreScaleIndex = 3;
+ int passedIndex = maxIndex;
for (int i = 0; i < maxPreScaleIndex; i++) {
final VolumeInfo volCur = new VolumeInfo.Builder(volMedia)
@@ -185,9 +188,12 @@
mAudioService.setDeviceVolume(volCur, bleDevice, mPackageName);
mTestLooper.dispatchAll();
+ if (absVolumeIndexFix()) {
+ passedIndex = i + 1;
+ }
// Stream volume changes
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
- AudioManager.STREAM_MUSIC, maxIndex,
+ AudioManager.STREAM_MUSIC, passedIndex,
AudioSystem.DEVICE_OUT_BLE_HEADSET);
}
@@ -197,8 +203,11 @@
mAudioService.setDeviceVolume(volIndex4, bleDevice, mPackageName);
mTestLooper.dispatchAll();
+ if (absVolumeIndexFix()) {
+ passedIndex = 4;
+ }
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
- AudioManager.STREAM_MUSIC, maxIndex,
+ AudioManager.STREAM_MUSIC, passedIndex,
AudioSystem.DEVICE_OUT_BLE_HEADSET);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index beed0a3d..c305fd9 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -39,8 +39,9 @@
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.KEYCODE_VOLUME_UP;
-import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
import static com.android.media.audio.Flags.FLAG_ABS_VOLUME_INDEX_FIX;
+import static com.android.media.audio.Flags.FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME;
+import static com.android.media.audio.Flags.absVolumeIndexFix;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
@@ -627,7 +628,6 @@
@Test
@RequiresFlagsEnabled(FLAG_DISABLE_PRESCALE_ABSOLUTE_VOLUME)
- @RequiresFlagsDisabled(FLAG_ABS_VOLUME_INDEX_FIX)
public void disablePreScaleAbsoluteVolume_checkIndex() throws Exception {
final int minIndex = mAm.getStreamMinVolume(STREAM_MUSIC);
final int maxIndex = mAm.getStreamMaxVolume(STREAM_MUSIC);
@@ -638,6 +638,7 @@
final AudioDeviceAttributes bleDevice = new AudioDeviceAttributes(
/*native type*/ AudioSystem.DEVICE_OUT_BLE_HEADSET, /*address*/ "bla");
final int maxPreScaleIndex = 3;
+ int passedIndex = maxIndex;
for (int i = 0; i < maxPreScaleIndex; i++) {
final VolumeInfo volCur = new VolumeInfo.Builder(volMedia)
@@ -646,9 +647,12 @@
mAudioService.setDeviceVolume(volCur, bleDevice, mContext.getOpPackageName());
mTestLooper.dispatchAll();
+ if (absVolumeIndexFix()) {
+ passedIndex = i + 1;
+ }
// Stream volume changes
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
- STREAM_MUSIC, maxIndex,
+ STREAM_MUSIC, passedIndex,
AudioSystem.DEVICE_OUT_BLE_HEADSET);
}
@@ -658,8 +662,11 @@
mAudioService.setDeviceVolume(volIndex4, bleDevice, mContext.getOpPackageName());
mTestLooper.dispatchAll();
+ if (absVolumeIndexFix()) {
+ passedIndex = 4;
+ }
verify(mSpyAudioSystem, atLeast(1)).setStreamVolumeIndexAS(
- STREAM_MUSIC, maxIndex,
+ STREAM_MUSIC, passedIndex,
AudioSystem.DEVICE_OUT_BLE_HEADSET);
}
diff --git a/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java b/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
index f91f77a..cdfc521 100644
--- a/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/pdb/PersistentDataBlockServiceTest.java
@@ -86,7 +86,6 @@
private File mDataBlockFile;
private File mFrpSecretFile;
private File mFrpSecretTmpFile;
- private String mOemUnlockPropertyValue;
private boolean mIsUpgradingFromPreV = false;
@Mock private UserManager mUserManager;
@@ -105,13 +104,6 @@
}
@Override
- void setProperty(String key, String value) {
- // Override to capture the value instead of actually setting the property.
- assertThat(key).isEqualTo("sys.oem_unlock_allowed");
- mOemUnlockPropertyValue = value;
- }
-
- @Override
boolean isUpgradingFromPreVRelease() {
return mIsUpgradingFromPreV;
}
@@ -598,7 +590,6 @@
mInterface.setOemUnlockEnabled(true);
assertThat(mInterface.getOemUnlockEnabled()).isTrue();
- assertThat(mOemUnlockPropertyValue).isEqualTo("1");
}
@Test
@@ -635,7 +626,6 @@
// The current implementation does not check digest before set or get the oem unlock bit.
tamperWithDigest();
mInterface.setOemUnlockEnabled(true);
- assertThat(mOemUnlockPropertyValue).isEqualTo("1");
tamperWithDigest();
assertThat(mInterface.getOemUnlockEnabled()).isTrue();
}
@@ -676,7 +666,6 @@
mInternalInterface.forceOemUnlockEnabled(true);
- assertThat(mOemUnlockPropertyValue).isEqualTo("1");
assertThat(readBackingFile(mPdbService.getOemUnlockDataOffset(), 1).array())
.isEqualTo(new byte[] { 1 });
}
diff --git a/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
new file mode 100644
index 0000000..6bd4279
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/supervision/SupervisionServiceTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.supervision
+
+import android.os.Bundle
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.server.LocalServices
+import com.android.server.pm.UserManagerInternal
+import com.google.common.truth.Truth.assertThat
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+/**
+ * Unit tests for {@link SupervisionService}.
+ * <p/>
+ * Run with <code>atest SupervisionServiceTest</code>.
+ */
+@RunWith(AndroidJUnit4::class)
+class SupervisionServiceTest {
+ companion object {
+ const val USER_ID = 100
+ }
+
+ private lateinit var service: SupervisionService
+
+ @Rule
+ @JvmField
+ val mocks: MockitoRule = MockitoJUnit.rule()
+
+ @Mock
+ private lateinit var mockUserManagerInternal: UserManagerInternal
+
+ @Before
+ fun setup() {
+ val context = InstrumentationRegistry.getInstrumentation().context
+
+ LocalServices.removeServiceForTest(UserManagerInternal::class.java)
+ LocalServices.addService(UserManagerInternal::class.java, mockUserManagerInternal)
+
+ service = SupervisionService(context)
+ }
+
+ @Test
+ fun testSetSupervisionEnabledForUser() {
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+
+ service.setSupervisionEnabledForUser(USER_ID, true)
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isTrue()
+
+ service.setSupervisionEnabledForUser(USER_ID, false)
+ assertThat(service.isSupervisionEnabledForUser(USER_ID)).isFalse()
+ }
+
+ @Test
+ fun testSetSupervisionLockscreenEnabledForUser() {
+ var userData = service.getUserDataLocked(USER_ID)
+ assertThat(userData.supervisionLockScreenEnabled).isFalse()
+ assertThat(userData.supervisionLockScreenOptions).isNull()
+
+ service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, true, Bundle())
+ userData = service.getUserDataLocked(USER_ID)
+ assertThat(userData.supervisionLockScreenEnabled).isTrue()
+ assertThat(userData.supervisionLockScreenOptions).isNotNull()
+
+ service.mInternal.setSupervisionLockscreenEnabledForUser(USER_ID, false, null)
+ userData = service.getUserDataLocked(USER_ID)
+ assertThat(userData.supervisionLockScreenEnabled).isFalse()
+ assertThat(userData.supervisionLockScreenOptions).isNull()
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 130690d..6c9015d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -16556,7 +16556,7 @@
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testSetCanBePromoted_granted() throws Exception {
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
@@ -16611,7 +16611,7 @@
mService.addNotification(r);
mService.addEnqueuedNotification(r1);
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
waitForIdle();
@@ -16632,7 +16632,7 @@
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testSetCanBePromoted_granted_onlyNotifiesOnce() throws Exception {
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
@@ -16651,9 +16651,9 @@
mService.addNotification(r);
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
waitForIdle();
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
waitForIdle();
ArgumentCaptor<NotificationRecord> captor =
@@ -16663,12 +16663,12 @@
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testSetCanBePromoted_revoked() throws Exception {
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
// start from true state
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
// qualifying posted notification
Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
@@ -16709,7 +16709,7 @@
mService.addNotification(r);
mService.addEnqueuedNotification(r1);
- mBinderService.setCanBePromoted(mPkg, mUid, false);
+ mBinderService.setCanBePromoted(mPkg, mUid, false, true);
waitForIdle();
@@ -16728,12 +16728,12 @@
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testSetCanBePromoted_revoked_onlyNotifiesOnce() throws Exception {
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
// start from true state
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
// qualifying posted notification
Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId())
@@ -16751,9 +16751,9 @@
mService.addNotification(r);
- mBinderService.setCanBePromoted(mPkg, mUid, false);
+ mBinderService.setCanBePromoted(mPkg, mUid, false, true);
waitForIdle();
- mBinderService.setCanBePromoted(mPkg, mUid, false);
+ mBinderService.setCanBePromoted(mPkg, mUid, false, true);
waitForIdle();
ArgumentCaptor<NotificationRecord> captor =
@@ -16763,10 +16763,10 @@
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testPostPromotableNotification() throws Exception {
- mBinderService.setCanBePromoted(mPkg, mUid, true);
- assertThat(mBinderService.canBePromoted(mPkg, mUid)).isTrue();
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
+ assertThat(mBinderService.appCanBePromoted(mPkg, mUid)).isTrue();
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
@@ -16776,7 +16776,6 @@
.setColor(Color.WHITE)
.setColorized(true)
.build();
- //assertThat(n.hasPromotableCharacteristics()).isTrue();
StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, 9, null, mUid, 0,
n, UserHandle.getUserHandleForUid(mUid), null, 0);
@@ -16794,7 +16793,7 @@
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testPostPromotableNotification_noPermission() throws Exception {
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
@@ -16822,9 +16821,9 @@
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testPostPromotableNotification_unimportantNotification() throws Exception {
- mBinderService.setCanBePromoted(mPkg, mUid, true);
+ mBinderService.setCanBePromoted(mPkg, mUid, true, true);
mContext.getTestablePermissions().setPermission(
android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED);
Notification n = new Notification.Builder(mContext, mMinChannel.getId())
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 7d63062..a0c0df8 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -62,6 +62,7 @@
import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA;
import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER;
import static com.android.server.notification.PreferencesHelper.DEFAULT_BUBBLE_PREFERENCE;
+import static com.android.server.notification.PreferencesHelper.LockableAppFields.USER_LOCKED_PROMOTABLE;
import static com.android.server.notification.PreferencesHelper.NOTIFICATION_CHANNEL_COUNT_LIMIT;
import static com.android.server.notification.PreferencesHelper.NOTIFICATION_CHANNEL_GROUP_COUNT_LIMIT;
import static com.android.server.notification.PreferencesHelper.UNKNOWN_UID;
@@ -643,7 +644,7 @@
mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true);
if (android.app.Flags.uiRichOngoing()) {
- mHelper.setCanBePromoted(PKG_N_MR1, UID_N_MR1, true);
+ mHelper.setCanBePromoted(PKG_N_MR1, UID_N_MR1, true, true);
}
ByteArrayOutputStream baos = writeXmlAndPurge(PKG_N_MR1, UID_N_MR1, false,
@@ -657,6 +658,8 @@
assertTrue(mXmlHelper.canShowBadge(PKG_N_MR1, UID_N_MR1));
if (android.app.Flags.uiRichOngoing()) {
assertThat(mXmlHelper.canBePromoted(PKG_N_MR1, UID_N_MR1)).isTrue();
+ assertThat(mXmlHelper.getAppLockedFields(PKG_N_MR1, UID_N_MR1) & USER_LOCKED_PROMOTABLE)
+ .isNotEqualTo(0);
}
assertEquals(channel1,
mXmlHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1.getId(), false));
@@ -6311,20 +6314,30 @@
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testNoAppHasPermissionToPromoteByDefault() {
mHelper.setShowBadge(PKG_P, UID_P, true);
assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse();
}
@Test
- @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING)
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testSetCanBePromoted() {
- mHelper.setCanBePromoted(PKG_P, UID_P, true);
+ mHelper.setCanBePromoted(PKG_P, UID_P, true, true);
assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
- mHelper.setCanBePromoted(PKG_P, UID_P, false);
+ mHelper.setCanBePromoted(PKG_P, UID_P, false, true);
assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse();
verify(mHandler, never()).requestSort();
}
+
+ @Test
+ @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
+ public void testSetCanBePromoted_allowlistNotOverrideUser() {
+ mHelper.setCanBePromoted(PKG_P, UID_P, true, true);
+ assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
+
+ mHelper.setCanBePromoted(PKG_P, UID_P, false, false);
+ assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
+ }
}
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
index c7a136a..23ee893 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
@@ -44,7 +44,8 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -52,12 +53,14 @@
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
+import android.app.IActivityManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManagerInternal;
import android.media.AudioManager;
import android.os.Handler;
@@ -79,12 +82,10 @@
import com.android.internal.util.test.FakeSettingsProvider;
import com.android.internal.util.test.FakeSettingsProviderRule;
-import com.android.server.LocalServices;
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.vibrator.VibrationSession.CallerInfo;
import com.android.server.vibrator.VibrationSession.Status;
-import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -101,8 +102,7 @@
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
- private static final int OLD_USER_ID = 123;
- private static final int NEW_USER_ID = 456;
+ private static final int USER_ID = 123;
private static final int UID = 1;
private static final int VIRTUAL_DEVICE_ID = 1;
private static final String SYSUI_PACKAGE_NAME = "sysui";
@@ -132,13 +132,12 @@
@Mock private VirtualDeviceManagerInternal mVirtualDeviceManagerInternalMock;
@Mock private PackageManagerInternal mPackageManagerInternalMock;
@Mock private AudioManager mAudioManagerMock;
+ @Mock private IActivityManager mActivityManagerMock;
@Mock private VibrationConfig mVibrationConfigMock;
private TestLooper mTestLooper;
private ContextWrapper mContextSpy;
private VibrationSettings mVibrationSettings;
- private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener;
- private BroadcastReceiver mRegisteredBatteryBroadcastReceiver;
@Before
public void setUp() throws Exception {
@@ -146,24 +145,21 @@
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext()));
ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy);
- when(mContextSpy.getContentResolver()).thenReturn(contentResolver);
- when(mContextSpy.getSystemService(eq(Context.AUDIO_SERVICE))).thenReturn(mAudioManagerMock);
- doAnswer(invocation -> {
- mRegisteredPowerModeListener = invocation.getArgument(0);
- return null;
- }).when(mPowerManagerInternalMock).registerLowPowerModeObserver(any());
+ doReturn(contentResolver).when(mContextSpy).getContentResolver();
+
+ // Make sure broadcast receivers are not registered for this test, to avoid flakes.
+ doReturn(null).when(mContextSpy)
+ .registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class), anyInt());
+
when(mPackageManagerInternalMock.getSystemUiServiceComponent())
.thenReturn(new ComponentName(SYSUI_PACKAGE_NAME, ""));
- removeServicesForTest();
- addServicesForTest();
-
setDefaultIntensity(VIBRATION_INTENSITY_MEDIUM);
setIgnoreVibrationsOnWirelessCharger(false);
- createSystemReadyVibrationSettings();
-
mockGoToSleep(/* goToSleepTime= */ 0, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT);
+
+ createSystemReadyVibrationSettings();
}
private void createSystemReadyVibrationSettings() {
@@ -177,37 +173,18 @@
setUserSetting(Settings.System.APPLY_RAMPING_RINGER, 0);
setRingerMode(AudioManager.RINGER_MODE_NORMAL);
- mVibrationSettings.onSystemReady();
- }
-
- private void removeServicesForTest() {
- LocalServices.removeServiceForTest(PowerManagerInternal.class);
- LocalServices.removeServiceForTest(PackageManagerInternal.class);
- LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class);
- }
-
- private void addServicesForTest() {
- LocalServices.addService(PowerManagerInternal.class, mPowerManagerInternalMock);
- LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
- LocalServices.addService(VirtualDeviceManagerInternal.class,
- mVirtualDeviceManagerInternalMock);
- }
-
- @After
- public void tearDown() throws Exception {
- removeServicesForTest();
+ mVibrationSettings.onSystemReady(mPackageManagerInternalMock, mPowerManagerInternalMock,
+ mActivityManagerMock, mVirtualDeviceManagerInternalMock, mAudioManagerMock);
}
@Test
public void create_withOnlyRequiredSystemServices() {
- // The only core services that we depend on are PowerManager and PackageManager
- removeServicesForTest();
- LocalServices.addService(PowerManagerInternal.class, mPowerManagerInternalMock);
- LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
-
VibrationSettings minimalVibrationSettings = new VibrationSettings(mContextSpy,
new Handler(mTestLooper.getLooper()), mVibrationConfigMock);
- minimalVibrationSettings.onSystemReady();
+
+ // The only core services that we depend on are Power, Package and Activity managers
+ minimalVibrationSettings.onSystemReady(mPackageManagerInternalMock,
+ mPowerManagerInternalMock, mActivityManagerMock, null, null);
}
@Test
@@ -215,8 +192,8 @@
mVibrationSettings.addListener(mListenerMock);
// Testing the broadcast flow manually.
- mVibrationSettings.mUserSwitchObserver.onUserSwitching(NEW_USER_ID);
- mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(NEW_USER_ID);
+ mVibrationSettings.mUserSwitchObserver.onUserSwitching(USER_ID);
+ mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(USER_ID);
verify(mListenerMock, times(2)).onChange();
}
@@ -226,9 +203,9 @@
mVibrationSettings.addListener(mListenerMock);
// Testing the broadcast flow manually.
- mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+ mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy,
new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
- mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+ mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy,
new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
verify(mListenerMock, times(2)).onChange();
@@ -250,9 +227,9 @@
mVibrationSettings.addListener(mListenerMock);
// Testing the broadcast flow manually.
- mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
- mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
- mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); // No change.
+ mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
+ mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
+ mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); // Noop.
verify(mListenerMock, times(2)).onChange();
}
@@ -268,9 +245,9 @@
// Trigger multiple observers manually.
mVibrationSettings.mSettingObserver.onChange(false);
- mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
- mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(NEW_USER_ID);
- mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+ mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
+ mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(USER_ID);
+ mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy,
new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
verifyNoMoreInteractions(mListenerMock);
@@ -311,11 +288,12 @@
@Test
public void wirelessChargingVibrationsEnabled_doesNotRegisterBatteryReceiver_allowsAnyUsage() {
- setBatteryReceiverRegistrationResult(getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS));
setIgnoreVibrationsOnWirelessCharger(false);
createSystemReadyVibrationSettings();
- assertNull(mRegisteredBatteryBroadcastReceiver);
+ verify(mContextSpy, never()).registerReceiver(any(BroadcastReceiver.class),
+ argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt());
+
for (int usage : ALL_USAGES) {
assertVibrationNotIgnoredForUsage(usage);
}
@@ -323,7 +301,6 @@
@Test
public void shouldIgnoreVibration_noBatteryIntentWhenSystemReady_allowsAnyUsage() {
- setBatteryReceiverRegistrationResult(null);
setIgnoreVibrationsOnWirelessCharger(true);
createSystemReadyVibrationSettings();
@@ -335,7 +312,10 @@
@Test
public void shouldIgnoreVibration_onNonWirelessChargerWhenSystemReady_allowsAnyUsage() {
Intent nonWirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_USB);
- setBatteryReceiverRegistrationResult(nonWirelessChargingIntent);
+ doReturn(nonWirelessChargingIntent).when(mContextSpy).registerReceiver(
+ any(BroadcastReceiver.class),
+ argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt());
+
setIgnoreVibrationsOnWirelessCharger(true);
createSystemReadyVibrationSettings();
@@ -347,7 +327,10 @@
@Test
public void shouldIgnoreVibration_onWirelessChargerWhenSystemReady_doesNotAllowFromAnyUsage() {
Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS);
- setBatteryReceiverRegistrationResult(wirelessChargingIntent);
+ doReturn(wirelessChargingIntent).when(mContextSpy).registerReceiver(
+ any(BroadcastReceiver.class),
+ argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt());
+
setIgnoreVibrationsOnWirelessCharger(true);
createSystemReadyVibrationSettings();
@@ -358,13 +341,12 @@
@Test
public void shouldIgnoreVibration_receivesWirelessChargingIntent_doesNotAllowFromAnyUsage() {
- Intent nonWirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_USB);
- setBatteryReceiverRegistrationResult(nonWirelessChargingIntent);
setIgnoreVibrationsOnWirelessCharger(true);
createSystemReadyVibrationSettings();
Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS);
- mRegisteredBatteryBroadcastReceiver.onReceive(mContextSpy, wirelessChargingIntent);
+ mVibrationSettings.mBatteryBroadcastReceiver.onReceive(
+ mContextSpy, wirelessChargingIntent);
for (int usage : ALL_USAGES) {
assertVibrationIgnoredForUsage(usage, Status.IGNORED_ON_WIRELESS_CHARGER);
@@ -373,17 +355,21 @@
@Test
public void shouldIgnoreVibration_receivesNonWirelessChargingIntent_allowsAnyUsage() {
- Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS);
- setBatteryReceiverRegistrationResult(wirelessChargingIntent);
setIgnoreVibrationsOnWirelessCharger(true);
createSystemReadyVibrationSettings();
+
+ Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS);
+ mVibrationSettings.mBatteryBroadcastReceiver.onReceive(
+ mContextSpy, wirelessChargingIntent);
+
// Check that initially, all usages are ignored due to the wireless charging.
for (int usage : ALL_USAGES) {
assertVibrationIgnoredForUsage(usage, Status.IGNORED_ON_WIRELESS_CHARGER);
}
Intent nonWirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_USB);
- mRegisteredBatteryBroadcastReceiver.onReceive(mContextSpy, nonWirelessChargingIntent);
+ mVibrationSettings.mBatteryBroadcastReceiver.onReceive(
+ mContextSpy, nonWirelessChargingIntent);
for (int usage : ALL_USAGES) {
assertVibrationNotIgnoredForUsage(usage);
@@ -400,7 +386,7 @@
USAGE_HARDWARE_FEEDBACK
));
- mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
+ mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE);
for (int usage : ALL_USAGES) {
if (expectedAllowedVibrations.contains(usage)) {
@@ -413,7 +399,7 @@
@Test
public void shouldIgnoreVibration_notInBatterySaverMode_allowsAnyUsage() {
- mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
+ mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE);
for (int usage : ALL_USAGES) {
assertVibrationNotIgnoredForUsage(usage);
@@ -596,7 +582,7 @@
// Testing the broadcast flow manually.
when(mAudioManagerMock.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT);
- mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+ mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy,
new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
assertVibrationIgnoredForUsage(USAGE_RINGTONE, Status.IGNORED_FOR_RINGER_MODE);
@@ -852,16 +838,15 @@
mVibrationSettings.getCurrentIntensity(USAGE_RINGTONE));
// Test early update of settings based on new user id.
- putUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW,
- NEW_USER_ID);
- mVibrationSettings.mUserSwitchObserver.onUserSwitching(NEW_USER_ID);
+ putUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW, USER_ID);
+ mVibrationSettings.mUserSwitchObserver.onUserSwitching(USER_ID);
assertEquals(VIBRATION_INTENSITY_LOW,
mVibrationSettings.getCurrentIntensity(USAGE_RINGTONE));
// Test later update of settings for UserHandle.USER_CURRENT.
putUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW,
UserHandle.USER_CURRENT);
- mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(NEW_USER_ID);
+ mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(USER_ID);
assertEquals(VIBRATION_INTENSITY_LOW,
mVibrationSettings.getCurrentIntensity(USAGE_RINGTONE));
}
@@ -1009,7 +994,7 @@
private void setRingerMode(int ringerMode) {
when(mAudioManagerMock.getRingerModeInternal()).thenReturn(ringerMode);
// Mock AudioManager broadcast of internal ringer mode change.
- mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy,
+ mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy,
new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
}
@@ -1024,14 +1009,6 @@
return new CallerInfo(attrs, uid, VIRTUAL_DEVICE_ID, opPkg, null);
}
- private void setBatteryReceiverRegistrationResult(Intent result) {
- doAnswer(invocation -> {
- mRegisteredBatteryBroadcastReceiver = invocation.getArgument(0);
- return result;
- }).when(mContextSpy).registerReceiver(any(BroadcastReceiver.class),
- argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt());
- }
-
private Intent getBatteryChangedIntent(int extraPluggedValue) {
Intent batteryIntent = new Intent(Intent.ACTION_BATTERY_CHANGED);
batteryIntent.putExtra(EXTRA_PLUGGED, extraPluggedValue);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 31cc50f1..e83a4b2 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -29,13 +29,11 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -96,8 +94,6 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;
@@ -187,11 +183,11 @@
mVibratorProviders.clear();
CombinedVibration effect = CombinedVibration.createParallel(
VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
- verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
+ verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
}
@Test
@@ -200,11 +196,11 @@
.addNext(2, VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
.addNext(3, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
.combine();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
- verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
+ verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
}
@Test
@@ -212,34 +208,34 @@
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
VibrationEffect effect = VibrationEffect.createOneShot(10, 100);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(expectedOneShot(10)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
}
@Test
public void vibrate_oneShotWithoutAmplitudeControl_runsVibrationWithDefaultAmplitude() {
VibrationEffect effect = VibrationEffect.createOneShot(10, 100);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(expectedOneShot(10)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
}
@@ -249,17 +245,17 @@
VibrationEffect effect = VibrationEffect.createWaveform(
new long[]{5, 5, 5}, new int[]{1, 2, 3}, -1);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(15L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(expectedOneShot(15)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(1, 2, 3),
mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
}
@@ -277,13 +273,13 @@
mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f);
CompletableFuture<Void> mRequestVibrationParamsFuture = CompletableFuture.completedFuture(
null);
- long vibrationId = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture,
+ HalVibration vibration = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture,
USAGE_RINGTONE);
waitForCompletion();
verify(mStatsLoggerMock, never()).logVibrationParamRequestTimeout(UID);
assertEquals(Arrays.asList(expectedOneShot(15)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
for (int i = 0; i < amplitudes.size(); i++) {
assertTrue(amplitudes.get(i) < 1 / 255f);
@@ -301,12 +297,13 @@
new long[]{5, 5, 5}, new int[]{1, 1, 1}, -1);
CompletableFuture<Void> neverCompletingFuture = new CompletableFuture<>();
- long vibrationId = startThreadAndDispatcher(effect, neverCompletingFuture, USAGE_RINGTONE);
+ HalVibration vibration = startThreadAndDispatcher(effect, neverCompletingFuture,
+ USAGE_RINGTONE);
waitForCompletion();
verify(mStatsLoggerMock).logVibrationParamRequestTimeout(UID);
assertEquals(Arrays.asList(expectedOneShot(15)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(1, 1, 1),
mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
}
@@ -319,13 +316,13 @@
int[] amplitudes = new int[]{1, 2, 3};
VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5, 5, 5}, amplitudes, 0);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(
waitUntil(() -> fakeVibrator.getAmplitudes().size() > 2 * amplitudes.length,
TEST_TIMEOUT_MILLIS));
// Vibration still running after 2 cycles.
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
assertTrue(mControllers.get(VIBRATOR_ID).isVibrating());
Vibration.EndInfo cancelVibrationInfo = new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED,
@@ -333,15 +330,15 @@
/* uid= */ 1, /* deviceId= */ -1, /* opPkg= */ null, /* reason= */ null));
mVibrationConductor.notifyCancelled(cancelVibrationInfo, /* immediate= */ false);
waitForCompletion();
- assertFalse(mThread.isRunningVibrationId(vibrationId));
+ assertFalse(mThread.isRunningVibrationId(vibration.id));
verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong());
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verifyCallbacksTriggered(vibrationId, cancelVibrationInfo);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_SUPERSEDED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
List<Float> playedAmplitudes = fakeVibrator.getAmplitudes();
- assertFalse(fakeVibrator.getEffectSegments(vibrationId).isEmpty());
+ assertFalse(fakeVibrator.getEffectSegments(vibration.id).isEmpty());
assertFalse(playedAmplitudes.isEmpty());
for (int i = 0; i < playedAmplitudes.size(); i++) {
@@ -358,17 +355,17 @@
int[] amplitudes = new int[]{1, 2, 3};
VibrationEffect effect = VibrationEffect.createWaveform(
new long[]{1, 10, 100}, amplitudes, 0);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> !fakeVibrator.getAmplitudes().isEmpty(), TEST_TIMEOUT_MILLIS));
mVibrationConductor.notifyCancelled(
new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
waitForCompletion();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(expectedOneShot(5000)),
- fakeVibrator.getEffectSegments(vibrationId));
+ fakeVibrator.getEffectSegments(vibration.id));
}
@Test
@@ -377,16 +374,16 @@
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[]{0, 100, 50, 100, 0, 0, 0, 50}, /* repeat= */ -1);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(300L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
- assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId))
+ assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id))
.isEqualTo(expectedOneShots(100L, 150L));
}
@@ -398,16 +395,16 @@
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[]{0, 100, 0, 50, 50, 0, 100, 50}, amplitudes,
/* repeat= */ -1);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(350L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
- assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId))
+ assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id))
.isEqualTo(expectedOneShots(200L, 50L));
}
@@ -420,7 +417,7 @@
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[]{0, 200, 50, 100, 0, 50, 50, 100}, /* repeat= */ 0);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
// We are expect this test to repeat the vibration effect twice, which would result in 5
// segments being played:
// 200ms ON
@@ -428,16 +425,17 @@
// 300ms ON (100ms + 200ms looping to the start and skipping first 0ms)
// 150ms ON (100ms + 50ms, skips 0ms)
// 300ms ON (100ms + 200ms looping to the start and skipping first 0ms)
- assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() >= 5,
+ assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibration.id).size() >= 5,
5000L + TEST_TIMEOUT_MILLIS));
mVibrationConductor.notifyCancelled(
new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
waitForCompletion();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
- assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).subList(0, 5))
+ assertThat(
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).subList(0, 5))
.isEqualTo(expectedOneShots(200L, 150L, 300L, 150L, 300L));
}
@@ -458,18 +456,18 @@
VibrationEffect repeatingEffect = VibrationEffect.startComposition()
.repeatEffectIndefinitely(effect)
.compose();
- long vibrationId = startThreadAndDispatcher(repeatingEffect);
+ HalVibration vibration = startThreadAndDispatcher(repeatingEffect);
- assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+ assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(),
TEST_TIMEOUT_MILLIS));
mVibrationConductor.notifyCancelled(
new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
waitForCompletion();
// PWLE size max was used to generate a single vibrate call with 10 segments.
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
- assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size());
+ assertEquals(10, fakeVibrator.getEffectSegments(vibration.id).size());
}
@Test
@@ -487,18 +485,18 @@
VibrationEffect repeatingEffect = VibrationEffect.startComposition()
.repeatEffectIndefinitely(effect)
.compose();
- long vibrationId = startThreadAndDispatcher(repeatingEffect);
+ HalVibration vibration = startThreadAndDispatcher(repeatingEffect);
- assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+ assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(),
TEST_TIMEOUT_MILLIS));
mVibrationConductor.notifyCancelled(
new Vibration.EndInfo(Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ false);
waitForCompletion();
// Composition size max was used to generate a single vibrate call with 10 primitives.
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
- assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size());
+ assertEquals(10, fakeVibrator.getEffectSegments(vibration.id).size());
}
@Test
@@ -510,17 +508,17 @@
int[] amplitudes = new int[]{1, 2, 3};
VibrationEffect effect = VibrationEffect.createWaveform(
new long[]{5000, 500, 50}, amplitudes, 0);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> !fakeVibrator.getAmplitudes().isEmpty(), TEST_TIMEOUT_MILLIS));
mVibrationConductor.notifyCancelled(
new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
waitForCompletion();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(expectedOneShot(5550)),
- fakeVibrator.getEffectSegments(vibrationId));
+ fakeVibrator.getEffectSegments(vibration.id));
}
@LargeTest
@@ -534,17 +532,17 @@
VibrationEffect effect = VibrationEffect.createWaveform(
/* timings= */ new long[]{expectedOnDuration - 100, 50},
/* amplitudes= */ new int[]{1, 2}, /* repeat= */ 0);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
- assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() > 1,
+ assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibration.id).size() > 1,
expectedOnDuration + TEST_TIMEOUT_MILLIS));
mVibrationConductor.notifyCancelled(
new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
waitForCompletion();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
- List<VibrationEffectSegment> effectSegments = fakeVibrator.getEffectSegments(vibrationId);
+ List<VibrationEffectSegment> effectSegments = fakeVibrator.getEffectSegments(vibration.id);
// First time, turn vibrator ON for the expected fixed duration.
assertEquals(expectedOnDuration, effectSegments.get(0).getDuration());
// Vibrator turns off in the middle of the second execution of the first step. Expect it to
@@ -567,11 +565,11 @@
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
.compose();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
TEST_TIMEOUT_MILLIS));
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
// Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
// fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -584,7 +582,7 @@
waitForCompletion(/* timeout= */ 50);
cancellingThread.join();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SETTINGS_UPDATE);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SETTINGS_UPDATE);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
}
@@ -597,11 +595,11 @@
mVibratorProviders.get(VIBRATOR_ID).setVendorEffectDuration(10 * TEST_TIMEOUT_MILLIS);
VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
TEST_TIMEOUT_MILLIS));
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
// Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
// fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -614,7 +612,7 @@
waitForCompletion(/* timeout= */ 50);
cancellingThread.join();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SETTINGS_UPDATE);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SETTINGS_UPDATE);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
}
@@ -624,11 +622,11 @@
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
VibrationEffect effect = VibrationEffect.createWaveform(new long[]{100}, new int[]{100}, 0);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
TEST_TIMEOUT_MILLIS));
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
// Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
// fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -641,7 +639,7 @@
waitForCompletion(/* timeout= */ 50);
cancellingThread.join();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
}
@@ -650,17 +648,17 @@
mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_THUD);
VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_THUD);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_THUD)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
}
@Test
@@ -671,31 +669,31 @@
HalVibration vibration = createVibration(CombinedVibration.createParallel(
VibrationEffect.get(VibrationEffect.EFFECT_CLICK)));
vibration.addFallback(VibrationEffect.EFFECT_CLICK, fallback);
- long vibrationId = startThreadAndDispatcher(vibration);
+ startThreadAndDispatcher(vibration);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(expectedOneShot(10)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
}
@Test
public void vibrate_singleVibratorPrebakedAndUnsupportedEffect_ignoresVibration() {
VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L));
verify(mManagerHooks, never()).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
- assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty());
+ verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
+ assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty());
}
@Test
@@ -704,17 +702,17 @@
mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID),
eq(PerformVendorEffectVibratorStep.VENDOR_EFFECT_MAX_DURATION_MS));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
- assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId))
+ assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibration.id))
.containsExactly(effect)
.inOrder();
}
@@ -730,18 +728,18 @@
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
.compose();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(40L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(
expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0),
expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 0)),
- fakeVibrator.getEffectSegments(vibrationId));
+ fakeVibrator.getEffectSegments(vibration.id));
}
@Test
@@ -749,14 +747,14 @@
VibrationEffect effect = VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f)
.compose();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L));
verify(mManagerHooks, never()).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED);
- assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty());
+ verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED);
+ assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty());
}
@Test
@@ -774,13 +772,13 @@
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f)
.compose();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
// Vibrator compose called twice.
- verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- assertEquals(3, fakeVibrator.getEffectSegments(vibrationId).size());
+ verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ assertEquals(3, fakeVibrator.getEffectSegments(vibration.id).size());
}
@Test
@@ -810,14 +808,14 @@
.build())
.addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK))
.compose();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
// Use first duration the vibrator is turned on since we cannot estimate the clicks.
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(
expectedOneShot(10),
@@ -829,7 +827,7 @@
expectedRamp(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.7f,
/* startFrequencyHz= */ 100, /* endFrequencyHz= */ 120, /* duration= */ 20),
expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes());
}
@@ -851,18 +849,18 @@
.compose();
HalVibration vibration = createVibration(CombinedVibration.createParallel(effect));
vibration.addFallback(VibrationEffect.EFFECT_TICK, fallback);
- long vibrationId = startThreadAndDispatcher(vibration);
+ startThreadAndDispatcher(vibration);
waitForCompletion();
// Use first duration the vibrator is turned on since we cannot estimate the clicks.
verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong());
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
List<VibrationEffectSegment> segments =
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId);
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id);
assertTrue("Wrong segments: " + segments, segments.size() >= 4);
assertTrue(segments.get(0) instanceof PrebakedSegment);
assertTrue(segments.get(1) instanceof PrimitiveSegment);
@@ -891,13 +889,13 @@
.addSustain(Duration.ofMillis(30))
.addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200))
.build();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(
expectedRamp(/* amplitude= */ 1, /* frequencyHz= */ 150, /* duration= */ 10),
@@ -907,8 +905,8 @@
expectedRamp(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.6f,
/* startFrequencyHz= */ 100, /* endFrequencyHz= */ 200,
/* duration= */ 40)),
- fakeVibrator.getEffectSegments(vibrationId));
- assertEquals(Arrays.asList(Braking.CLAB), fakeVibrator.getBraking(vibrationId));
+ fakeVibrator.getEffectSegments(vibration.id));
+ assertEquals(Arrays.asList(Braking.CLAB), fakeVibrator.getBraking(vibration.id));
}
@Test
@@ -932,15 +930,15 @@
.addTransition(Duration.ofMillis(40), targetAmplitude(0.7f), targetFrequency(200))
.addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200))
.build();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
// Vibrator compose called 3 times with 2 segments instead of 2 times with 3 segments.
// Using best split points instead of max-packing PWLEs.
- verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- assertEquals(6, fakeVibrator.getEffectSegments(vibrationId).size());
+ verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ assertEquals(6, fakeVibrator.getEffectSegments(vibration.id).size());
}
@Test
@@ -949,28 +947,28 @@
fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> fakeVibrator.getAmplitudes().size() > 2, TEST_TIMEOUT_MILLIS));
// Vibration still running after 2 cycles.
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
assertTrue(mControllers.get(VIBRATOR_ID).isVibrating());
mVibrationConductor.binderDied();
waitForCompletion();
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BINDER_DIED);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BINDER_DIED);
}
@Test
public void vibrate_singleVibrator_skipsSyncedCallbacks() {
mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
- long vibrationId = startThreadAndDispatcher(VibrationEffect.createOneShot(10, 100));
+ HalVibration vibration = startThreadAndDispatcher(VibrationEffect.createOneShot(10, 100));
waitForCompletion();
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
verify(mManagerHooks, never()).prepareSyncedVibration(anyLong(), any());
verify(mManagerHooks, never()).triggerSyncedVibration(anyLong());
verify(mManagerHooks, never()).cancelSyncedVibration();
@@ -984,18 +982,18 @@
.addVibrator(VIBRATOR_ID, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
.addVibrator(2, VibrationEffect.get(VibrationEffect.EFFECT_TICK))
.combine();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_TICK)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
}
@Test
@@ -1007,26 +1005,26 @@
CombinedVibration effect = CombinedVibration.createParallel(
VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
- verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
- verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+ verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
+ verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(1).isVibrating());
assertFalse(mControllers.get(2).isVibrating());
assertFalse(mControllers.get(3).isVibrating());
VibrationEffectSegment expected = expectedPrebaked(VibrationEffect.EFFECT_CLICK);
assertEquals(Arrays.asList(expected),
- mVibratorProviders.get(1).getEffectSegments(vibrationId));
+ mVibratorProviders.get(1).getEffectSegments(vibration.id));
assertEquals(Arrays.asList(expected),
- mVibratorProviders.get(2).getEffectSegments(vibrationId));
+ mVibratorProviders.get(2).getEffectSegments(vibration.id));
assertEquals(Arrays.asList(expected),
- mVibratorProviders.get(3).getEffectSegments(vibrationId));
+ mVibratorProviders.get(3).getEffectSegments(vibration.id));
}
@Test
@@ -1049,32 +1047,32 @@
new long[]{10, 10}, new int[]{1, 2}, -1))
.addVibrator(4, composed)
.combine();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
- verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
- verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
- verify(mControllerCallbacks).onComplete(eq(4), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+ verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
+ verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+ verify(mControllerCallbacks).onComplete(eq(4), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(1).isVibrating());
assertFalse(mControllers.get(2).isVibrating());
assertFalse(mControllers.get(3).isVibrating());
assertFalse(mControllers.get(4).isVibrating());
assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
- mVibratorProviders.get(1).getEffectSegments(vibrationId));
+ mVibratorProviders.get(1).getEffectSegments(vibration.id));
assertEquals(Arrays.asList(expectedOneShot(10)),
- mVibratorProviders.get(2).getEffectSegments(vibrationId));
+ mVibratorProviders.get(2).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(100), mVibratorProviders.get(2).getAmplitudes());
assertEquals(Arrays.asList(expectedOneShot(20)),
- mVibratorProviders.get(3).getEffectSegments(vibrationId));
+ mVibratorProviders.get(3).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(1, 2), mVibratorProviders.get(3).getAmplitudes());
assertEquals(Arrays.asList(
expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
- mVibratorProviders.get(4).getEffectSegments(vibrationId));
+ mVibratorProviders.get(4).getEffectSegments(vibration.id));
}
@Test
@@ -1094,13 +1092,13 @@
.addNext(1, VibrationEffect.createOneShot(10, 100), /* delay= */ 50)
.addNext(2, composed, /* delay= */ 50)
.combine();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
InOrder controllerVerifier = inOrder(mControllerCallbacks);
- controllerVerifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
- controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
- controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
+ controllerVerifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+ controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+ controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
InOrder batteryVerifier = inOrder(mManagerHooks);
batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
@@ -1110,19 +1108,19 @@
batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L));
batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(1).isVibrating());
assertFalse(mControllers.get(2).isVibrating());
assertFalse(mControllers.get(3).isVibrating());
assertEquals(Arrays.asList(expectedOneShot(10)),
- mVibratorProviders.get(1).getEffectSegments(vibrationId));
+ mVibratorProviders.get(1).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(100), mVibratorProviders.get(1).getAmplitudes());
assertEquals(Arrays.asList(
expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
- mVibratorProviders.get(2).getEffectSegments(vibrationId));
+ mVibratorProviders.get(2).getEffectSegments(vibration.id));
assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
- mVibratorProviders.get(3).getEffectSegments(vibrationId));
+ mVibratorProviders.get(3).getEffectSegments(vibration.id));
}
@Test
@@ -1143,30 +1141,29 @@
CombinedVibration effect = CombinedVibration.createParallel(composed);
// We create the HalVibration here to obtain the vibration id and use it to mock the
// required response when calling triggerSyncedVibration.
- HalVibration halVibration = createVibration(effect);
- long vibrationId = halVibration.id;
- when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true);
- startThreadAndDispatcher(halVibration);
+ HalVibration vibration = createVibration(effect);
+ when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(true);
+ startThreadAndDispatcher(vibration);
assertTrue(waitUntil(
- () -> !mVibratorProviders.get(1).getEffectSegments(vibrationId).isEmpty()
- && !mVibratorProviders.get(2).getEffectSegments(vibrationId).isEmpty(),
+ () -> !mVibratorProviders.get(1).getEffectSegments(vibration.id).isEmpty()
+ && !mVibratorProviders.get(2).getEffectSegments(vibration.id).isEmpty(),
TEST_TIMEOUT_MILLIS));
mVibrationConductor.notifySyncedVibrationComplete();
waitForCompletion();
long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_COMPOSE;
verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
- verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId));
+ verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id));
verify(mManagerHooks, never()).cancelSyncedVibration();
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
VibrationEffectSegment expected = expectedPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 100);
assertEquals(Arrays.asList(expected),
- mVibratorProviders.get(1).getEffectSegments(vibrationId));
+ mVibratorProviders.get(1).getEffectSegments(vibration.id));
assertEquals(Arrays.asList(expected),
- mVibratorProviders.get(2).getEffectSegments(vibrationId));
+ mVibratorProviders.get(2).getEffectSegments(vibration.id));
}
@Test
@@ -1190,10 +1187,9 @@
.combine();
// We create the HalVibration here to obtain the vibration id and use it to mock the
// required response when calling triggerSyncedVibration.
- HalVibration halVibration = createVibration(effect);
- long vibrationId = halVibration.id;
- when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true);
- startThreadAndDispatcher(halVibration);
+ HalVibration vibration = createVibration(effect);
+ when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(true);
+ startThreadAndDispatcher(vibration);
waitForCompletion();
long expectedCap = IVibratorManager.CAP_SYNC
@@ -1204,9 +1200,9 @@
| IVibratorManager.CAP_MIXED_TRIGGER_PERFORM
| IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE;
verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
- verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId));
+ verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id));
verify(mManagerHooks, never()).cancelSyncedVibration();
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
}
@Test
@@ -1221,19 +1217,19 @@
.addVibrator(1, VibrationEffect.createOneShot(10, 100))
.addVibrator(2, VibrationEffect.createWaveform(new long[]{5}, new int[]{200}, -1))
.combine();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_ON;
verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
- verify(mManagerHooks, never()).triggerSyncedVibration(eq(vibrationId));
+ verify(mManagerHooks, never()).triggerSyncedVibration(eq(vibration.id));
verify(mManagerHooks, never()).cancelSyncedVibration();
assertEquals(Arrays.asList(expectedOneShot(10)),
- mVibratorProviders.get(1).getEffectSegments(vibrationId));
+ mVibratorProviders.get(1).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(100), mVibratorProviders.get(1).getAmplitudes());
assertEquals(Arrays.asList(expectedOneShot(5)),
- mVibratorProviders.get(2).getEffectSegments(vibrationId));
+ mVibratorProviders.get(2).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(200), mVibratorProviders.get(2).getAmplitudes());
}
@@ -1250,10 +1246,9 @@
.combine();
// We create the HalVibration here to obtain the vibration id and use it to mock the
// required response when calling triggerSyncedVibration.
- HalVibration halVibration = createVibration(effect);
- long vibrationId = halVibration.id;
- when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(false);
- startThreadAndDispatcher(halVibration);
+ HalVibration vibration = createVibration(effect);
+ when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(false);
+ startThreadAndDispatcher(vibration);
waitForCompletion();
long expectedCap = IVibratorManager.CAP_SYNC
@@ -1262,7 +1257,7 @@
| IVibratorManager.CAP_MIXED_TRIGGER_ON
| IVibratorManager.CAP_MIXED_TRIGGER_PERFORM;
verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds));
- verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId));
+ verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id));
verify(mManagerHooks).cancelSyncedVibration();
assertTrue(mVibratorProviders.get(1).getAmplitudes().isEmpty());
}
@@ -1282,7 +1277,7 @@
.addVibrator(3, VibrationEffect.createWaveform(
new long[]{60}, new int[]{6}, -1))
.combine();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
// All vibrators are turned on in parallel.
assertTrue(waitUntil(
@@ -1295,20 +1290,20 @@
verify(mManagerHooks).noteVibratorOn(eq(UID), eq(80L));
verify(mManagerHooks).noteVibratorOff(eq(UID));
- verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId));
- verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId));
- verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id));
+ verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id));
+ verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertFalse(mControllers.get(1).isVibrating());
assertFalse(mControllers.get(2).isVibrating());
assertFalse(mControllers.get(3).isVibrating());
assertEquals(Arrays.asList(expectedOneShot(25)),
- mVibratorProviders.get(1).getEffectSegments(vibrationId));
+ mVibratorProviders.get(1).getEffectSegments(vibration.id));
assertEquals(Arrays.asList(expectedOneShot(80)),
- mVibratorProviders.get(2).getEffectSegments(vibrationId));
+ mVibratorProviders.get(2).getEffectSegments(vibration.id));
assertEquals(Arrays.asList(expectedOneShot(60)),
- mVibratorProviders.get(3).getEffectSegments(vibrationId));
+ mVibratorProviders.get(3).getEffectSegments(vibration.id));
assertEquals(expectedAmplitudes(1, 2, 3), mVibratorProviders.get(1).getAmplitudes());
assertEquals(expectedAmplitudes(4, 5), mVibratorProviders.get(2).getAmplitudes());
assertEquals(expectedAmplitudes(6), mVibratorProviders.get(3).getAmplitudes());
@@ -1327,17 +1322,11 @@
CombinedVibration.createParallel(
VibrationEffect.createOneShot(
expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
- CountDownLatch vibrationCompleteLatch = new CountDownLatch(1);
- doAnswer(unused -> {
- vibrationCompleteLatch.countDown();
- return null;
- }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any());
startThreadAndDispatcher(vibration);
long startTime = SystemClock.elapsedRealtime();
- assertTrue(vibrationCompleteLatch.await(expectedDuration + TEST_TIMEOUT_MILLIS,
- TimeUnit.MILLISECONDS));
+ vibration.waitForEnd();
long vibrationEndTime = SystemClock.elapsedRealtime();
waitForCompletion(rampDownDuration + TEST_TIMEOUT_MILLIS);
@@ -1363,17 +1352,11 @@
CombinedVibration.createParallel(
VibrationEffect.createOneShot(
expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
- CountDownLatch vibrationCompleteLatch = new CountDownLatch(1);
- doAnswer(unused -> {
- vibrationCompleteLatch.countDown();
- return null;
- }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any());
startThreadAndDispatcher(vibration);
long startTime = SystemClock.elapsedRealtime();
- assertTrue(vibrationCompleteLatch.await(callbackDelay + TEST_TIMEOUT_MILLIS,
- TimeUnit.MILLISECONDS));
+ vibration.waitForEnd();
long vibrationEndTime = SystemClock.elapsedRealtime();
waitForCompletion(TEST_TIMEOUT_MILLIS);
@@ -1397,17 +1380,11 @@
CombinedVibration.createParallel(
VibrationEffect.createOneShot(
expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE)));
- CountDownLatch vibrationCompleteLatch = new CountDownLatch(1);
- doAnswer(unused -> {
- vibrationCompleteLatch.countDown();
- return null;
- }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any());
startThreadAndDispatcher(vibration);
long startTime = SystemClock.elapsedRealtime();
- assertTrue(vibrationCompleteLatch.await(callbackTimeout + TEST_TIMEOUT_MILLIS,
- TimeUnit.MILLISECONDS));
+ vibration.waitForEnd();
long vibrationEndTime = SystemClock.elapsedRealtime();
waitForCompletion(callbackDelay + TEST_TIMEOUT_MILLIS);
@@ -1461,11 +1438,11 @@
fakeVibrator.setOnLatency(latency);
VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
- assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(),
+ assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(),
TEST_TIMEOUT_MILLIS));
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
// Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
// fail at waitForCompletion(cancellingThread).
@@ -1480,7 +1457,7 @@
// After the vibrator call ends the vibration is cancelled and the vibrator is turned off.
waitForCompletion(/* timeout= */ latency + TEST_TIMEOUT_MILLIS);
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
}
@@ -1500,10 +1477,10 @@
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100)
.compose())
.combine();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS));
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
// Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
// fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -1516,7 +1493,7 @@
waitForCompletion(/* timeout= */ 50);
cancellingThread.join();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
assertFalse(mControllers.get(1).isVibrating());
assertFalse(mControllers.get(2).isVibrating());
}
@@ -1534,10 +1511,10 @@
.addVibrator(1, VibrationEffect.createVendorEffect(createTestVendorData()))
.addVibrator(2, VibrationEffect.createVendorEffect(createTestVendorData()))
.combine();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS));
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
// Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
// fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -1550,7 +1527,7 @@
waitForCompletion(/* timeout= */ 50);
cancellingThread.join();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
assertFalse(mControllers.get(1).isVibrating());
assertFalse(mControllers.get(2).isVibrating());
}
@@ -1566,12 +1543,12 @@
new long[]{100, 100}, new int[]{1, 2}, 0))
.addVibrator(2, VibrationEffect.createOneShot(100, 100))
.combine();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> mControllers.get(1).isVibrating()
&& mControllers.get(2).isVibrating(),
TEST_TIMEOUT_MILLIS));
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
// Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should
// fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately.
@@ -1584,7 +1561,7 @@
waitForCompletion(/* timeout= */ 50);
cancellingThread.join();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF);
assertFalse(mControllers.get(1).isVibrating());
assertFalse(mControllers.get(2).isVibrating());
}
@@ -1592,19 +1569,17 @@
@Test
public void vibrate_binderDied_cancelsVibration() throws Exception {
VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
TEST_TIMEOUT_MILLIS));
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
mVibrationConductor.binderDied();
waitForCompletion();
- verify(mVibrationToken).linkToDeath(same(mVibrationConductor), eq(0));
- verify(mVibrationToken).unlinkToDeath(same(mVibrationConductor), eq(0));
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BINDER_DIED);
- assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty());
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BINDER_DIED);
+ assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty());
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
}
@@ -1615,15 +1590,15 @@
VibrationEffect effect = VibrationEffect.createWaveform(
new long[]{5, 5, 5}, new int[]{60, 120, 240}, -1);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
// Duration extended for 5 + 5 + 5 + 15.
assertEquals(Arrays.asList(expectedOneShot(30)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
assertTrue(amplitudes.size() > 3);
assertEquals(expectedAmplitudes(60, 120, 240), amplitudes.subList(0, 3));
@@ -1633,24 +1608,24 @@
}
@Test
- public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds() {
+ public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds()
+ throws Exception {
when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(10_000);
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
VibrationEffect effect = VibrationEffect.createOneShot(10, 200);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
// Vibration completed but vibrator not yet released.
- verify(mManagerHooks, timeout(TEST_TIMEOUT_MILLIS)).onVibrationCompleted(eq(vibrationId),
- eq(new Vibration.EndInfo(Status.FINISHED)));
+ vibration.waitForEnd();
verify(mManagerHooks, never()).onVibrationThreadReleased(anyLong());
// Thread still running ramp down.
- assertTrue(mThread.isRunningVibrationId(vibrationId));
+ assertTrue(mThread.isRunningVibrationId(vibration.id));
// Duration extended for 10 + 10000.
assertEquals(Arrays.asList(expectedOneShot(10_010)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
// Will stop the ramp down right away.
mVibrationConductor.notifyCancelled(
@@ -1658,9 +1633,8 @@
waitForCompletion();
// Does not cancel already finished vibration, but releases vibrator.
- verify(mManagerHooks, never()).onVibrationCompleted(eq(vibrationId),
- eq(new Vibration.EndInfo(Status.CANCELLED_BY_SETTINGS_UPDATE)));
- verify(mManagerHooks).onVibrationThreadReleased(vibrationId);
+ assertThat(vibration.getStatus()).isNotEqualTo(Status.CANCELLED_BY_SETTINGS_UPDATE);
+ verify(mManagerHooks).onVibrationThreadReleased(vibration.id);
}
@Test
@@ -1670,18 +1644,18 @@
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
VibrationEffect effect = VibrationEffect.createOneShot(10_000, 240);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(),
TEST_TIMEOUT_MILLIS));
mVibrationConductor.notifyCancelled(
new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
waitForCompletion();
- verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER);
+ verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER);
// Duration extended for 10000 + 15.
assertEquals(Arrays.asList(expectedOneShot(10_015)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes();
assertTrue(amplitudes.size() > 1);
for (int i = 1; i < amplitudes.size(); i++) {
@@ -1696,14 +1670,14 @@
mVibratorProviders.get(VIBRATOR_ID).setSupportedEffects(VibrationEffect.EFFECT_CLICK);
VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
}
@@ -1714,13 +1688,13 @@
mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS);
VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData());
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
- assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId))
+ assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibration.id))
.containsExactly(effect)
.inOrder();
assertThat(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()).isEmpty();
@@ -1737,15 +1711,15 @@
VibrationEffect effect = VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
.compose();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertEquals(
Arrays.asList(expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
- mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId));
+ mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id));
assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty());
}
@@ -1764,14 +1738,14 @@
VibrationEffect effect = VibrationEffect.startWaveform()
.addTransition(Duration.ofMillis(1), targetAmplitude(1))
.build();
- long vibrationId = startThreadAndDispatcher(effect);
+ HalVibration vibration = startThreadAndDispatcher(effect);
waitForCompletion();
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId));
- verifyCallbacksTriggered(vibrationId, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id));
+ verifyCallbacksTriggered(vibration, Status.FINISHED);
assertEquals(Arrays.asList(expectedRamp(0, 1, 150, 150, 1)),
- fakeVibrator.getEffectSegments(vibrationId));
+ fakeVibrator.getEffectSegments(vibration.id));
assertTrue(fakeVibrator.getAmplitudes().isEmpty());
}
@@ -1796,69 +1770,68 @@
VibrationEffect effect4 = VibrationEffect.createOneShot(8000, 100);
VibrationEffect effect5 = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
- long vibrationId1 = startThreadAndDispatcher(effect1);
+ HalVibration vibration1 = startThreadAndDispatcher(effect1);
waitForCompletion();
- verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibrationId1);
- verifyCallbacksTriggered(vibrationId1, Status.FINISHED);
- long vibrationId2 = startThreadAndDispatcher(effect2);
+ HalVibration vibration2 = startThreadAndDispatcher(effect2);
// Effect2 won't complete on its own. Cancel it after a couple of repeats.
Thread.sleep(150); // More than two TICKs.
mVibrationConductor.notifyCancelled(
new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false);
waitForCompletion();
- long vibrationId3 = startThreadAndDispatcher(effect3);
+ HalVibration vibration3 = startThreadAndDispatcher(effect3);
waitForCompletion();
// Effect4 is a long oneshot, but it gets cancelled as fast as possible.
long start4 = System.currentTimeMillis();
- long vibrationId4 = startThreadAndDispatcher(effect4);
+ HalVibration vibration4 = startThreadAndDispatcher(effect4);
mVibrationConductor.notifyCancelled(
new Vibration.EndInfo(Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ true);
waitForCompletion();
long duration4 = System.currentTimeMillis() - start4;
// Effect5 is to show that things keep going after the immediate cancel.
- long vibrationId5 = startThreadAndDispatcher(effect5);
+ HalVibration vibration5 = startThreadAndDispatcher(effect5);
waitForCompletion();
FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
assertFalse(mControllers.get(VIBRATOR_ID).isVibrating());
// Effect1
- verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibrationId1);
- verifyCallbacksTriggered(vibrationId1, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibration1.id);
+ verifyCallbacksTriggered(vibration1, Status.FINISHED);
assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
- fakeVibrator.getEffectSegments(vibrationId1));
+ fakeVibrator.getEffectSegments(vibration1.id));
// Effect2: repeating, cancelled.
- verify(mControllerCallbacks, atLeast(2)).onComplete(VIBRATOR_ID, vibrationId2);
- verifyCallbacksTriggered(vibrationId2, Status.CANCELLED_BY_USER);
+ verify(mControllerCallbacks, atLeast(2)).onComplete(VIBRATOR_ID, vibration2.id);
+ verifyCallbacksTriggered(vibration2, Status.CANCELLED_BY_USER);
// The exact count of segments might vary, so just check that there's more than 2 and
// all elements are the same segment.
- List<VibrationEffectSegment> actualSegments2 = fakeVibrator.getEffectSegments(vibrationId2);
+ List<VibrationEffectSegment> actualSegments2 =
+ fakeVibrator.getEffectSegments(vibration2.id);
assertTrue(actualSegments2.size() + " > 2", actualSegments2.size() > 2);
for (VibrationEffectSegment segment : actualSegments2) {
assertEquals(expectedPrebaked(VibrationEffect.EFFECT_TICK), segment);
}
// Effect3
- verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId3));
- verifyCallbacksTriggered(vibrationId3, Status.FINISHED);
+ verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration3.id));
+ verifyCallbacksTriggered(vibration3, Status.FINISHED);
assertEquals(Arrays.asList(
expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)),
- fakeVibrator.getEffectSegments(vibrationId3));
+ fakeVibrator.getEffectSegments(vibration3.id));
// Effect4: cancelled quickly.
- verifyCallbacksTriggered(vibrationId4, Status.CANCELLED_BY_SCREEN_OFF);
+ verifyCallbacksTriggered(vibration4, Status.CANCELLED_BY_SCREEN_OFF);
assertTrue("Tested duration=" + duration4, duration4 < 2000);
// Effect5: played normally after effect4, which may or may not have played.
assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)),
- fakeVibrator.getEffectSegments(vibrationId5));
+ fakeVibrator.getEffectSegments(vibration5.id));
}
private void mockVibrators(int... vibratorIds) {
@@ -1875,19 +1848,19 @@
mVibrationSettings.mSettingObserver.onChange(false);
}
- private long startThreadAndDispatcher(VibrationEffect effect) {
+ private HalVibration startThreadAndDispatcher(VibrationEffect effect) {
return startThreadAndDispatcher(CombinedVibration.createParallel(effect));
}
- private long startThreadAndDispatcher(CombinedVibration effect) {
+ private HalVibration startThreadAndDispatcher(CombinedVibration effect) {
return startThreadAndDispatcher(createVibration(effect));
}
- private long startThreadAndDispatcher(HalVibration vib) {
+ private HalVibration startThreadAndDispatcher(HalVibration vib) {
return startThreadAndDispatcher(vib, /* requestVibrationParamsFuture= */ null);
}
- private long startThreadAndDispatcher(VibrationEffect effect,
+ private HalVibration startThreadAndDispatcher(VibrationEffect effect,
CompletableFuture<Void> requestVibrationParamsFuture, int usage) {
VibrationAttributes attrs = new VibrationAttributes.Builder()
.setUsage(usage)
@@ -1898,14 +1871,14 @@
return startThreadAndDispatcher(vib, requestVibrationParamsFuture);
}
- private long startThreadAndDispatcher(HalVibration vib,
+ private HalVibration startThreadAndDispatcher(HalVibration vib,
CompletableFuture<Void> requestVibrationParamsFuture) {
mControllers = createVibratorControllers();
DeviceAdapter deviceAdapter = new DeviceAdapter(mVibrationSettings, mControllers);
mVibrationConductor = new VibrationStepConductor(vib, mVibrationSettings, deviceAdapter,
mVibrationScaler, mStatsLoggerMock, requestVibrationParamsFuture, mManagerHooks);
assertTrue(mThread.runVibrationOnVibrationThread(mVibrationConductor));
- return mVibrationConductor.getVibration().id;
+ return mVibrationConductor.getVibration();
}
private boolean waitUntil(BooleanSupplier predicate, long timeout)
@@ -1994,13 +1967,9 @@
.collect(Collectors.toList());
}
- private void verifyCallbacksTriggered(long vibrationId, Status expectedStatus) {
- verifyCallbacksTriggered(vibrationId, new Vibration.EndInfo(expectedStatus));
- }
-
- private void verifyCallbacksTriggered(long vibrationId, Vibration.EndInfo expectedEndInfo) {
- verify(mManagerHooks).onVibrationCompleted(eq(vibrationId), eq(expectedEndInfo));
- verify(mManagerHooks).onVibrationThreadReleased(vibrationId);
+ private void verifyCallbacksTriggered(HalVibration vibration, Status expectedStatus) {
+ assertThat(vibration.getStatus()).isEqualTo(expectedStatus);
+ verify(mManagerHooks).onVibrationThreadReleased(vibration.id);
}
private static final class TestLooperAutoDispatcher extends Thread {
diff --git a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
index 1c33116..6f9c890 100644
--- a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java
@@ -22,6 +22,7 @@
import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS;
import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE;
+import android.platform.test.annotations.DisableFlags;
import android.view.ViewConfiguration;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -39,6 +40,7 @@
*/
@MediumTest
@RunWith(AndroidJUnit4.class)
+@DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_PRESS_GESTURES)
public class CombinationKeyTests extends ShortcutKeyTestBase {
private static final long A11Y_KEY_HOLD_MILLIS = 3500;
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
index 487390d..8b5f68a 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyCombinationManagerTests.java
@@ -71,12 +71,12 @@
new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
KEYCODE_POWER) {
@Override
- void execute() {
+ public void execute() {
mAction1Triggered.countDown();
}
@Override
- void cancel() {
+ public void cancel() {
}
});
@@ -85,21 +85,21 @@
new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
KEYCODE_VOLUME_UP) {
@Override
- boolean preCondition() {
+ public boolean preCondition() {
return mPreCondition;
}
@Override
- void execute() {
+ public void execute() {
mAction2Triggered.countDown();
}
@Override
- void cancel() {
+ public void cancel() {
}
@Override
- long getKeyInterceptDelayMs() {
+ public long getKeyInterceptDelayMs() {
return 0;
}
});
@@ -115,12 +115,12 @@
};
@Override
- void execute() {
+ public void execute() {
mHandler.postDelayed(mAction, SCHEDULE_TIME);
}
@Override
- void cancel() {
+ public void cancel() {
mHandler.removeCallbacks(mAction);
}
});
@@ -235,12 +235,12 @@
new KeyCombinationManager.TwoKeysCombinationRule(KEYCODE_VOLUME_DOWN,
KEYCODE_POWER) {
@Override
- void execute() {
+ public void execute() {
mAction1Triggered.countDown();
}
@Override
- void cancel() {
+ public void cancel() {
}
};
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index 3d978e4..cdb4542 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -22,10 +22,13 @@
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_ALL_APPS;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_ASSIST;
import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_HOME_NOTIFICATION_PANEL;
+import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS;
+import static com.android.server.policy.PhoneWindowManager.POWER_VOLUME_UP_BEHAVIOR_MUTE;
import static com.android.server.policy.PhoneWindowManager.SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL;
import android.hardware.input.KeyGestureEvent;
import android.os.RemoteException;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.KeyEvent;
@@ -56,88 +59,15 @@
private static final int CTRL_ON = MODIFIER.get(KeyEvent.KEYCODE_CTRL_LEFT);
@Keep
- private static Object[][] shortcutTestArguments() {
+ private static Object[][] shortcutTestArgumentsNotMigratedToKeyGestureController() {
// testName, testKeys, expectedKeyGestureType, expectedKey, expectedModifierState
return new Object[][]{
- {"Meta + H -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_H},
- KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_H, META_ON},
- {"Meta + Enter -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
- KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_ENTER,
- META_ON},
{"HOME key -> Open Home", new int[]{KeyEvent.KEYCODE_HOME},
KeyGestureEvent.KEY_GESTURE_TYPE_HOME,
KeyEvent.KEYCODE_HOME, 0},
- {"RECENT_APPS key -> Open Overview", new int[]{KeyEvent.KEYCODE_RECENT_APPS},
- KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS,
- KeyEvent.KEYCODE_RECENT_APPS, 0},
- {"Meta + Tab -> Open Overview", new int[]{META_KEY, KeyEvent.KEYCODE_TAB},
- KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
- META_ON},
- {"Alt + Tab -> Open Overview", new int[]{ALT_KEY, KeyEvent.KEYCODE_TAB},
- KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
- ALT_ON},
{"BACK key -> Go back", new int[]{KeyEvent.KEYCODE_BACK},
KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
KeyEvent.KEYCODE_BACK, 0},
- {"Meta + Escape -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_ESCAPE},
- KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_ESCAPE,
- META_ON},
- {"Meta + Left arrow -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
- KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DPAD_LEFT,
- META_ON},
- {"Meta + Del -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DEL},
- KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DEL, META_ON},
- {"APP_SWITCH key -> Open App switcher", new int[]{KeyEvent.KEYCODE_APP_SWITCH},
- KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
- KeyEvent.KEYCODE_APP_SWITCH, 0},
- {"ASSIST key -> Launch assistant", new int[]{KeyEvent.KEYCODE_ASSIST},
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
- KeyEvent.KEYCODE_ASSIST, 0},
- {"Meta + A -> Launch assistant", new int[]{META_KEY, KeyEvent.KEYCODE_A},
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_A,
- META_ON},
- {"VOICE_ASSIST key -> Launch Voice Assistant",
- new int[]{KeyEvent.KEYCODE_VOICE_ASSIST},
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT,
- KeyEvent.KEYCODE_VOICE_ASSIST, 0},
- {"Meta + I -> Launch System Settings", new int[]{META_KEY, KeyEvent.KEYCODE_I},
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
- KeyEvent.KEYCODE_I, META_ON},
- {"Meta + N -> Toggle Notification panel", new int[]{META_KEY, KeyEvent.KEYCODE_N},
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
- KeyEvent.KEYCODE_N, META_ON},
- {"NOTIFICATION key -> Toggle Notification Panel",
- new int[]{KeyEvent.KEYCODE_NOTIFICATION},
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
- KeyEvent.KEYCODE_NOTIFICATION,
- 0},
- {"Meta + Ctrl + S -> Take Screenshot",
- new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
- KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, KeyEvent.KEYCODE_S,
- META_ON | CTRL_ON},
- {"Meta + / -> Open Shortcut Helper", new int[]{META_KEY, KeyEvent.KEYCODE_SLASH},
- KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
- KeyEvent.KEYCODE_SLASH, META_ON},
- {"BRIGHTNESS_UP key -> Increase Brightness",
- new int[]{KeyEvent.KEYCODE_BRIGHTNESS_UP},
- KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
- KeyEvent.KEYCODE_BRIGHTNESS_UP, 0},
- {"BRIGHTNESS_DOWN key -> Decrease Brightness",
- new int[]{KeyEvent.KEYCODE_BRIGHTNESS_DOWN},
- KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
- KeyEvent.KEYCODE_BRIGHTNESS_DOWN, 0},
- {"KEYBOARD_BACKLIGHT_UP key -> Increase Keyboard Backlight",
- new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP},
- KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, 0},
- {"KEYBOARD_BACKLIGHT_DOWN key -> Decrease Keyboard Backlight",
- new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN},
- KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, 0},
- {"KEYBOARD_BACKLIGHT_TOGGLE key -> Toggle Keyboard Backlight",
- new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE},
- KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, 0},
{"VOLUME_UP key -> Increase Volume", new int[]{KeyEvent.KEYCODE_VOLUME_UP},
KeyGestureEvent.KEY_GESTURE_TYPE_VOLUME_UP,
KeyEvent.KEYCODE_VOLUME_UP, 0},
@@ -147,53 +77,9 @@
{"VOLUME_MUTE key -> Mute Volume", new int[]{KeyEvent.KEYCODE_VOLUME_MUTE},
KeyGestureEvent.KEY_GESTURE_TYPE_VOLUME_MUTE,
KeyEvent.KEYCODE_VOLUME_MUTE, 0},
- {"ALL_APPS key -> Open App Drawer",
- new int[]{KeyEvent.KEYCODE_ALL_APPS},
- KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
- KeyEvent.KEYCODE_ALL_APPS, 0},
- {"SEARCH key -> Launch Search Activity", new int[]{KeyEvent.KEYCODE_SEARCH},
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
- KeyEvent.KEYCODE_SEARCH, 0},
- {"LANGUAGE_SWITCH key -> Switch Keyboard Language",
- new int[]{KeyEvent.KEYCODE_LANGUAGE_SWITCH},
- KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
- KeyEvent.KEYCODE_LANGUAGE_SWITCH, 0},
- {"META key -> Open App Drawer", new int[]{META_KEY},
- KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, META_KEY,
- META_ON},
- {"Meta + Alt -> Toggle CapsLock", new int[]{META_KEY, ALT_KEY},
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, ALT_KEY,
- META_ON | ALT_ON},
- {"Alt + Meta -> Toggle CapsLock", new int[]{ALT_KEY, META_KEY},
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, META_KEY,
- META_ON | ALT_ON},
- {"CAPS_LOCK key -> Toggle CapsLock", new int[]{KeyEvent.KEYCODE_CAPS_LOCK},
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
- KeyEvent.KEYCODE_CAPS_LOCK, 0},
{"MUTE key -> Mute System Microphone", new int[]{KeyEvent.KEYCODE_MUTE},
KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_MUTE, KeyEvent.KEYCODE_MUTE,
0},
- {"Meta + Ctrl + DPAD_UP -> Split screen navigation",
- new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_UP},
- KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
- KeyEvent.KEYCODE_DPAD_UP,
- META_ON | CTRL_ON},
- {"Meta + Ctrl + DPAD_LEFT -> Split screen navigation",
- new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
- KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT,
- KeyEvent.KEYCODE_DPAD_LEFT,
- META_ON | CTRL_ON},
- {"Meta + Ctrl + DPAD_RIGHT -> Split screen navigation",
- new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_RIGHT},
- KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT,
- KeyEvent.KEYCODE_DPAD_RIGHT,
- META_ON | CTRL_ON},
- {"Meta + L -> Lock Homescreen", new int[]{META_KEY, KeyEvent.KEYCODE_L},
- KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, KeyEvent.KEYCODE_L,
- META_ON},
- {"Meta + Ctrl + N -> Open Notes", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_N},
- KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, KeyEvent.KEYCODE_N,
- META_ON | CTRL_ON},
{"POWER key -> Toggle Power", new int[]{KeyEvent.KEYCODE_POWER},
KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_POWER, KeyEvent.KEYCODE_POWER,
0},
@@ -279,7 +165,130 @@
{"Meta + S -> Launch Default Messaging App",
new int[]{META_KEY, KeyEvent.KEYCODE_S},
KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING,
- KeyEvent.KEYCODE_S, META_ON},
+ KeyEvent.KEYCODE_S, META_ON}};
+ }
+
+ @Keep
+ private static Object[][] shortcutTestArgumentsMigratedToKeyGestureController() {
+ // testName, testKeys, expectedKeyGestureType, expectedKey, expectedModifierState
+ return new Object[][]{
+ {"Meta + H -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_H},
+ KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_H, META_ON},
+ {"Meta + Enter -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
+ KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyEvent.KEYCODE_ENTER,
+ META_ON},
+ {"RECENT_APPS key -> Open Overview", new int[]{KeyEvent.KEYCODE_RECENT_APPS},
+ KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS,
+ KeyEvent.KEYCODE_RECENT_APPS, 0},
+ {"Meta + Tab -> Open Overview", new int[]{META_KEY, KeyEvent.KEYCODE_TAB},
+ KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
+ META_ON},
+ {"Alt + Tab -> Open Overview", new int[]{ALT_KEY, KeyEvent.KEYCODE_TAB},
+ KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyEvent.KEYCODE_TAB,
+ ALT_ON},
+ {"Meta + Escape -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_ESCAPE},
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_ESCAPE,
+ META_ON},
+ {"Meta + Left arrow -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DPAD_LEFT,
+ META_ON},
+ {"Meta + Del -> Go back", new int[]{META_KEY, KeyEvent.KEYCODE_DEL},
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyEvent.KEYCODE_DEL, META_ON},
+ {"APP_SWITCH key -> Open App switcher", new int[]{KeyEvent.KEYCODE_APP_SWITCH},
+ KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
+ KeyEvent.KEYCODE_APP_SWITCH, 0},
+ {"ASSIST key -> Launch assistant", new int[]{KeyEvent.KEYCODE_ASSIST},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
+ KeyEvent.KEYCODE_ASSIST, 0},
+ {"Meta + A -> Launch assistant", new int[]{META_KEY, KeyEvent.KEYCODE_A},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_A,
+ META_ON},
+ {"VOICE_ASSIST key -> Launch Voice Assistant",
+ new int[]{KeyEvent.KEYCODE_VOICE_ASSIST},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT,
+ KeyEvent.KEYCODE_VOICE_ASSIST, 0},
+ {"Meta + I -> Launch System Settings", new int[]{META_KEY, KeyEvent.KEYCODE_I},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
+ KeyEvent.KEYCODE_I, META_ON},
+ {"Meta + N -> Toggle Notification panel", new int[]{META_KEY, KeyEvent.KEYCODE_N},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+ KeyEvent.KEYCODE_N, META_ON},
+ {"NOTIFICATION key -> Toggle Notification Panel",
+ new int[]{KeyEvent.KEYCODE_NOTIFICATION},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+ KeyEvent.KEYCODE_NOTIFICATION,
+ 0},
+ {"Meta + Ctrl + S -> Take Screenshot",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, KeyEvent.KEYCODE_S,
+ META_ON | CTRL_ON},
+ {"Meta + / -> Open Shortcut Helper", new int[]{META_KEY, KeyEvent.KEYCODE_SLASH},
+ KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
+ KeyEvent.KEYCODE_SLASH, META_ON},
+ {"BRIGHTNESS_UP key -> Increase Brightness",
+ new int[]{KeyEvent.KEYCODE_BRIGHTNESS_UP},
+ KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
+ KeyEvent.KEYCODE_BRIGHTNESS_UP, 0},
+ {"BRIGHTNESS_DOWN key -> Decrease Brightness",
+ new int[]{KeyEvent.KEYCODE_BRIGHTNESS_DOWN},
+ KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN,
+ KeyEvent.KEYCODE_BRIGHTNESS_DOWN, 0},
+ {"KEYBOARD_BACKLIGHT_UP key -> Increase Keyboard Backlight",
+ new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP},
+ KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP, 0},
+ {"KEYBOARD_BACKLIGHT_DOWN key -> Decrease Keyboard Backlight",
+ new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN},
+ KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN, 0},
+ {"KEYBOARD_BACKLIGHT_TOGGLE key -> Toggle Keyboard Backlight",
+ new int[]{KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE},
+ KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE, 0},
+ {"ALL_APPS key -> Open App Drawer",
+ new int[]{KeyEvent.KEYCODE_ALL_APPS},
+ KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
+ KeyEvent.KEYCODE_ALL_APPS, 0},
+ {"SEARCH key -> Launch Search Activity", new int[]{KeyEvent.KEYCODE_SEARCH},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
+ KeyEvent.KEYCODE_SEARCH, 0},
+ {"LANGUAGE_SWITCH key -> Switch Keyboard Language",
+ new int[]{KeyEvent.KEYCODE_LANGUAGE_SWITCH},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+ KeyEvent.KEYCODE_LANGUAGE_SWITCH, 0},
+ {"META key -> Open App Drawer", new int[]{META_KEY},
+ KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, META_KEY,
+ META_ON},
+ {"Meta + Alt -> Toggle CapsLock", new int[]{META_KEY, ALT_KEY},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, ALT_KEY,
+ META_ON | ALT_ON},
+ {"Alt + Meta -> Toggle CapsLock", new int[]{ALT_KEY, META_KEY},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, META_KEY,
+ META_ON | ALT_ON},
+ {"CAPS_LOCK key -> Toggle CapsLock", new int[]{KeyEvent.KEYCODE_CAPS_LOCK},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
+ KeyEvent.KEYCODE_CAPS_LOCK, 0},
+ {"Meta + Ctrl + DPAD_UP -> Split screen navigation",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_UP},
+ KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
+ KeyEvent.KEYCODE_DPAD_UP,
+ META_ON | CTRL_ON},
+ {"Meta + Ctrl + DPAD_LEFT -> Split screen navigation",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_LEFT},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT,
+ KeyEvent.KEYCODE_DPAD_LEFT,
+ META_ON | CTRL_ON},
+ {"Meta + Ctrl + DPAD_RIGHT -> Split screen navigation",
+ new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_RIGHT},
+ KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT,
+ KeyEvent.KEYCODE_DPAD_RIGHT,
+ META_ON | CTRL_ON},
+ {"Meta + L -> Lock Homescreen", new int[]{META_KEY, KeyEvent.KEYCODE_L},
+ KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, KeyEvent.KEYCODE_L,
+ META_ON},
+ {"Meta + Ctrl + N -> Open Notes", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_N},
+ KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, KeyEvent.KEYCODE_N,
+ META_ON | CTRL_ON},
{"Meta + Ctrl + DPAD_DOWN -> Enter desktop mode",
new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_DOWN},
KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE,
@@ -296,72 +305,14 @@
new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_NOTIFICATION_PANEL,
KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
KeyEvent.KEYCODE_HOME, 0},
- {"Long press META + ENTER -> Toggle Notification panel",
- new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
- LONG_PRESS_HOME_NOTIFICATION_PANEL,
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
- KeyEvent.KEYCODE_ENTER,
- META_ON},
- {"Long press META + H -> Toggle Notification panel",
- new int[]{META_KEY, KeyEvent.KEYCODE_H}, LONG_PRESS_HOME_NOTIFICATION_PANEL,
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
- KeyEvent.KEYCODE_H, META_ON},
{"Long press HOME key -> Launch assistant",
new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_ASSIST,
KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
KeyEvent.KEYCODE_HOME, 0},
- {"Long press META + ENTER -> Launch assistant",
- new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, LONG_PRESS_HOME_ASSIST,
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT,
- KeyEvent.KEYCODE_ENTER, META_ON},
- {"Long press META + H -> Launch assistant",
- new int[]{META_KEY, KeyEvent.KEYCODE_H}, LONG_PRESS_HOME_ASSIST,
- KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, KeyEvent.KEYCODE_H,
- META_ON},
{"Long press HOME key -> Open App Drawer",
new int[]{KeyEvent.KEYCODE_HOME}, LONG_PRESS_HOME_ALL_APPS,
KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
- KeyEvent.KEYCODE_HOME, 0},
- {"Long press META + ENTER -> Open App Drawer",
- new int[]{META_KEY, KeyEvent.KEYCODE_ENTER}, LONG_PRESS_HOME_ALL_APPS,
- KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
- KeyEvent.KEYCODE_ENTER, META_ON},
- {"Long press META + H -> Open App Drawer",
- new int[]{META_KEY, KeyEvent.KEYCODE_H},
- LONG_PRESS_HOME_ALL_APPS,
- KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS,
- KeyEvent.KEYCODE_H, META_ON}};
- }
-
- @Keep
- private static Object[][] doubleTapOnHomeTestArguments() {
- // testName, testKeys, doubleTapOnHomeBehavior, expectedKeyGestureType, expectedKey,
- // expectedModifierState
- return new Object[][]{
- {"Double tap HOME -> Open App switcher",
- new int[]{KeyEvent.KEYCODE_HOME}, DOUBLE_TAP_HOME_RECENT_SYSTEM_UI,
- KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, KeyEvent.KEYCODE_HOME,
- 0},
- {"Double tap META + ENTER -> Open App switcher",
- new int[]{META_KEY, KeyEvent.KEYCODE_ENTER},
- DOUBLE_TAP_HOME_RECENT_SYSTEM_UI,
- KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
- KeyEvent.KEYCODE_ENTER, META_ON},
- {"Double tap META + H -> Open App switcher",
- new int[]{META_KEY, KeyEvent.KEYCODE_H}, DOUBLE_TAP_HOME_RECENT_SYSTEM_UI,
- KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, KeyEvent.KEYCODE_H,
- META_ON}};
- }
-
- @Keep
- private static Object[][] settingsKeyTestArguments() {
- // testName, testKeys, settingsKeyBehavior, expectedKeyGestureType, expectedKey,
- // expectedModifierState
- return new Object[][]{
- {"SETTINGS key -> Toggle Notification panel", new int[]{KeyEvent.KEYCODE_SETTINGS},
- SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL,
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
- KeyEvent.KEYCODE_SETTINGS, 0}};
+ KeyEvent.KEYCODE_HOME, 0}};
}
@Before
@@ -381,8 +332,18 @@
}
@Test
- @Parameters(method = "shortcutTestArguments")
- public void testShortcut(String testName, int[] testKeys,
+ @Parameters(method = "shortcutTestArgumentsNotMigratedToKeyGestureController")
+ public void testShortcuts_notMigratedToKeyGestureController(String testName,
+ int[] testKeys, @KeyGestureEvent.KeyGestureType int expectedKeyGestureType,
+ int expectedKey, int expectedModifierState) {
+ testShortcutInternal(testName, testKeys, expectedKeyGestureType, expectedKey,
+ expectedModifierState);
+ }
+
+ @Test
+ @Parameters(method = "shortcutTestArgumentsMigratedToKeyGestureController")
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+ public void testShortcuts_migratedToKeyGestureController(String testName, int[] testKeys,
@KeyGestureEvent.KeyGestureType int expectedKeyGestureType, int expectedKey,
int expectedModifierState) {
testShortcutInternal(testName, testKeys, expectedKeyGestureType, expectedKey,
@@ -402,31 +363,29 @@
}
@Test
- @Parameters(method = "doubleTapOnHomeTestArguments")
- public void testDoubleTapOnHomeBehavior(String testName, int[] testKeys,
- int doubleTapOnHomeBehavior,
- @KeyGestureEvent.KeyGestureType int expectedKeyGestureType, int expectedKey,
- int expectedModifierState) {
- mPhoneWindowManager.overriderDoubleTapOnHomeBehavior(doubleTapOnHomeBehavior);
- sendKeyCombination(testKeys, 0 /* duration */);
- sendKeyCombination(testKeys, 0 /* duration */);
+ public void testDoubleTapOnHomeBehavior_AppSwitchBehavior() {
+ mPhoneWindowManager.overriderDoubleTapOnHomeBehavior(DOUBLE_TAP_HOME_RECENT_SYSTEM_UI);
+ sendKeyCombination(new int[]{KeyEvent.KEYCODE_HOME}, 0 /* duration */);
+ sendKeyCombination(new int[]{KeyEvent.KEYCODE_HOME}, 0 /* duration */);
mPhoneWindowManager.assertKeyGestureCompleted(
- new int[]{expectedKey}, expectedModifierState, expectedKeyGestureType,
- "Failed while executing " + testName);
+ new int[]{KeyEvent.KEYCODE_HOME}, /* modifierState = */0,
+ KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH,
+ "Failed while executing Double tap HOME -> Open App switcher");
}
@Test
- @Parameters(method = "settingsKeyTestArguments")
- public void testSettingsKey(String testName, int[] testKeys, int settingsKeyBehavior,
- @KeyGestureEvent.KeyGestureType int expectedKeyGestureType, int expectedKey,
- int expectedModifierState) {
- mPhoneWindowManager.overrideSettingsKeyBehavior(settingsKeyBehavior);
- testShortcutInternal(testName, testKeys, expectedKeyGestureType, expectedKey,
- expectedModifierState);
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
+ public void testSettingsKey_ToggleNotificationBehavior() {
+ mPhoneWindowManager.overrideSettingsKeyBehavior(SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL);
+ testShortcutInternal("SETTINGS key -> Toggle Notification panel",
+ new int[]{KeyEvent.KEYCODE_SETTINGS},
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+ KeyEvent.KEYCODE_SETTINGS, 0);
}
@Test
@EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testBugreportShortcutPress() {
testShortcutInternal("Meta + Ctrl + Del -> Trigger bug report",
new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DEL},
@@ -599,4 +558,145 @@
sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH));
mPhoneWindowManager.assertLaunchSearch();
}
+
+ @Test
+ public void testKeyGestureScreenshotChord() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ mPhoneWindowManager.moveTimeForward(500);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ mPhoneWindowManager.assertTakeScreenshotCalled();
+ }
+
+ @Test
+ public void testKeyGestureScreenshotChordCancelled() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD));
+ mPhoneWindowManager.assertTakeScreenshotNotCalled();
+ }
+
+ @Test
+ public void testKeyGestureAccessibilityShortcutChord() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.moveTimeForward(5000);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.assertAccessibilityKeychordCalled();
+ }
+
+ @Test
+ public void testKeyGestureAccessibilityShortcutChordCancelled() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
+ }
+
+ @Test
+ public void testKeyGestureRingerToggleChord() {
+ mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ mPhoneWindowManager.moveTimeForward(500);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ mPhoneWindowManager.assertVolumeMute();
+ }
+
+ @Test
+ public void testKeyGestureRingerToggleChordCancelled() {
+ mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD));
+ mPhoneWindowManager.assertVolumeNotMuted();
+ }
+
+ @Test
+ public void testKeyGestureGlobalAction() {
+ mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ mPhoneWindowManager.moveTimeForward(500);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ mPhoneWindowManager.assertShowGlobalActionsCalled();
+ }
+
+ @Test
+ public void testKeyGestureGlobalActionCancelled() {
+ mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS);
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS));
+ mPhoneWindowManager.assertShowGlobalActionsNotCalled();
+ }
+
+ @Test
+ public void testKeyGestureAccessibilityTvShortcutChord() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.moveTimeForward(5000);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.assertAccessibilityKeychordCalled();
+ }
+
+ @Test
+ public void testKeyGestureAccessibilityTvShortcutChordCancelled() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
+ mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
+ }
+
+ @Test
+ public void testKeyGestureTvTriggerBugReport() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ mPhoneWindowManager.moveTimeForward(1000);
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ mPhoneWindowManager.assertBugReportTakenForTv();
+ }
+
+ @Test
+ public void testKeyGestureTvTriggerBugReportCancelled() {
+ Assert.assertTrue(
+ sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ Assert.assertTrue(
+ sendKeyGestureEventCancel(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
+ mPhoneWindowManager.assertBugReportNotTakenForTv();
+ }
+
+ @Test
+ public void testKeyGestureAccessibilityShortcut() {
+ Assert.assertTrue(
+ sendKeyGestureEventComplete(
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT));
+ mPhoneWindowManager.assertAccessibilityKeychordCalled();
+ }
+
+ @Test
+ public void testKeyGestureCloseAllDialogs() {
+ Assert.assertTrue(
+ sendKeyGestureEventComplete(KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS));
+ mPhoneWindowManager.assertCloseAllDialogs();
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
index 43171f8..c186a03 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
@@ -119,6 +119,7 @@
* ALT + TAB to show recent apps.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testAltTab() {
mPhoneWindowManager.overrideStatusBarManagerInternal();
sendKeyCombination(new int[]{KEYCODE_ALT_LEFT, KEYCODE_TAB}, 0);
@@ -129,6 +130,7 @@
* CTRL + SPACE to switch keyboard layout.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testCtrlSpace() {
sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SPACE}, /* duration= */ 0,
ANY_DISPLAY_ID);
@@ -139,6 +141,7 @@
* CTRL + SHIFT + SPACE to switch keyboard layout backwards.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testCtrlShiftSpace() {
sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_SHIFT_LEFT, KEYCODE_SPACE},
/* duration= */ 0, ANY_DISPLAY_ID);
@@ -149,6 +152,7 @@
* CTRL + ALT + Z to enable accessibility service.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testCtrlAltZ() {
sendKeyCombination(new int[]{KEYCODE_CTRL_LEFT, KEYCODE_ALT_LEFT, KEYCODE_Z}, 0);
mPhoneWindowManager.assertAccessibilityKeychordCalled();
@@ -158,6 +162,7 @@
* META + CTRL+ S to take screenshot.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaCtrlS() {
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_S}, 0);
mPhoneWindowManager.assertTakeScreenshotCalled();
@@ -167,6 +172,7 @@
* META + N to expand notification panel.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaN() throws RemoteException {
mPhoneWindowManager.overrideTogglePanel();
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_N}, 0);
@@ -177,6 +183,7 @@
* META + SLASH to toggle shortcuts menu.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaSlash() {
mPhoneWindowManager.overrideStatusBarManagerInternal();
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_SLASH}, 0);
@@ -187,6 +194,7 @@
* META + ALT to toggle Cap Lock.
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaAlt() {
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ALT_LEFT}, 0);
mPhoneWindowManager.assertToggleCapsLock();
@@ -196,6 +204,7 @@
* META + H to go to homescreen
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaH() {
mPhoneWindowManager.overrideLaunchHome();
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_H}, 0);
@@ -206,6 +215,7 @@
* META + ENTER to go to homescreen
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testMetaEnter() {
mPhoneWindowManager.overrideLaunchHome();
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ENTER}, 0);
@@ -216,6 +226,7 @@
* Sends a KEYCODE_BRIGHTNESS_DOWN event and validates the brightness is decreased as expected;
*/
@Test
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testKeyCodeBrightnessDown() {
float[] currentBrightness = new float[]{0.1f, 0.05f, 0.0f};
float[] newBrightness = new float[]{0.065738f, 0.0275134f, 0.0f};
@@ -231,9 +242,9 @@
* Sends a KEYCODE_SCREENSHOT and validates screenshot is taken if flag is enabled
*/
@Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE)
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testTakeScreenshot_flagEnabled() {
- mSetFlagsRule.enableFlags(com.android.hardware.input.Flags
- .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
mPhoneWindowManager.assertTakeScreenshotCalled();
}
@@ -242,9 +253,9 @@
* Sends a KEYCODE_SCREENSHOT and validates screenshot is not taken if flag is disabled
*/
@Test
+ @DisableFlags({com.android.hardware.input.Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE,
+ com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER})
public void testTakeScreenshot_flagDisabled() {
- mSetFlagsRule.disableFlags(com.android.hardware.input.Flags
- .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE);
sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0);
mPhoneWindowManager.assertTakeScreenshotNotCalled();
}
@@ -254,6 +265,7 @@
*/
@Test
@EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+ @DisableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
public void testTakeBugReport_flagEnabled() throws RemoteException {
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_DEL}, 0);
mPhoneWindowManager.assertTakeBugreport(true);
@@ -263,7 +275,8 @@
* META+CTRL+BACKSPACE for taking a bugreport does nothing when the flag is disabledd.
*/
@Test
- @DisableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+ @DisableFlags({com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT,
+ com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER})
public void testTakeBugReport_flagDisabled() throws RemoteException {
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_CTRL_LEFT, KEYCODE_DEL}, 0);
mPhoneWindowManager.assertTakeBugreport(false);
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 50b7db4..9e47a00 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -241,6 +241,13 @@
KeyGestureEvent.ACTION_GESTURE_COMPLETE).build());
}
+ boolean sendKeyGestureEventCancel(int gestureType) {
+ return mPhoneWindowManager.sendKeyGestureEvent(
+ new KeyGestureEvent.Builder().setKeyGestureType(gestureType).setAction(
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE).setFlags(
+ KeyGestureEvent.FLAG_CANCELLED).build());
+ }
+
boolean sendKeyGestureEventComplete(int gestureType, int modifierState) {
return mPhoneWindowManager.sendKeyGestureEvent(
new KeyGestureEvent.Builder().setModifierState(modifierState).setKeyGestureType(
@@ -276,7 +283,7 @@
if ((actions & ACTION_PASS_TO_USER) != 0) {
if (0 == mPhoneWindowManager.interceptKeyBeforeDispatching(keyEvent)) {
if (!mDispatchedKeyHandler.onKeyDispatched(keyEvent)) {
- mPhoneWindowManager.dispatchUnhandledKey(keyEvent);
+ mPhoneWindowManager.interceptUnhandledKey(keyEvent);
}
}
}
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 98401b3..1aa9087 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -418,8 +418,8 @@
mKeyEventPolicyFlags);
}
- void dispatchUnhandledKey(KeyEvent event) {
- mPhoneWindowManager.dispatchUnhandledKey(mInputToken, event, FLAG_INTERACTIVE);
+ void interceptUnhandledKey(KeyEvent event) {
+ mPhoneWindowManager.interceptUnhandledKey(event, mInputToken);
}
boolean sendKeyGestureEvent(KeyGestureEvent event) {
@@ -657,16 +657,36 @@
verify(mPowerManager).userActivity(anyLong(), anyBoolean());
}
+ void assertShowGlobalActionsNotCalled() {
+ mTestLooper.dispatchAll();
+ verify(mGlobalActions, never()).showDialog(anyBoolean(), anyBoolean());
+ verify(mPowerManager, never()).userActivity(anyLong(), anyBoolean());
+ }
+
void assertVolumeMute() {
mTestLooper.dispatchAll();
verify(mAudioManagerInternal).silenceRingerModeInternal(eq("volume_hush"));
}
+ void assertVolumeNotMuted() {
+ mTestLooper.dispatchAll();
+ verify(mAudioManagerInternal, never()).silenceRingerModeInternal(any());
+ }
+
void assertAccessibilityKeychordCalled() {
mTestLooper.dispatchAll();
verify(mAccessibilityShortcutController).performAccessibilityShortcut();
}
+ void assertAccessibilityKeychordNotCalled() {
+ mTestLooper.dispatchAll();
+ verify(mAccessibilityShortcutController, never()).performAccessibilityShortcut();
+ }
+
+ void assertCloseAllDialogs() {
+ verify(mContext).closeSystemDialogs();
+ }
+
void assertDreamRequest() {
mTestLooper.dispatchAll();
verify(mDreamManagerInternal).requestDream();
@@ -809,6 +829,16 @@
}
+ void assertBugReportTakenForTv() {
+ mTestLooper.dispatchAll();
+ verify(mPhoneWindowManager).requestBugreportForTv();
+ }
+
+ void assertBugReportNotTakenForTv() {
+ mTestLooper.dispatchAll();
+ verify(mPhoneWindowManager, never()).requestBugreportForTv();
+ }
+
void assertTogglePanel() throws RemoteException {
mTestLooper.dispatchAll();
verify(mPhoneWindowManager.mStatusBarService).togglePanel();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 8227ed9..92205f39 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -230,6 +230,10 @@
mDisplayContent.setIgnoreOrientationRequest(enabled);
}
+ void setTopActivityOrganizedTask() {
+ doReturn(mTaskStack.top()).when(mActivityStack.top()).getOrganizedTask();
+ }
+
void setTopTaskInMultiWindowMode(boolean inMultiWindowMode) {
doReturn(inMultiWindowMode).when(mTaskStack.top()).inMultiWindowMode();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
index d66c21a..b91a5b7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java
@@ -28,7 +28,7 @@
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import android.compat.testing.PlatformCompatChangeRule;
import android.platform.test.annotations.DisableFlags;
@@ -218,7 +218,7 @@
}
@Test
- @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testShouldApplyCameraCompatFreeformTreatment_flagIsDisabled_returnsFalse() {
runTestScenario((robot) -> {
robot.activity().createActivityWithComponentInNewTask();
@@ -229,7 +229,7 @@
@Test
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() {
runTestScenario((robot) -> {
robot.activity().createActivityWithComponentInNewTask();
@@ -240,7 +240,7 @@
@Test
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testShouldApplyCameraCompatFreeformTreatment_disabledByOverride_returnsFalse() {
runTestScenario((robot) -> {
robot.activity().createActivityWithComponentInNewTask();
@@ -250,7 +250,7 @@
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
runTestScenario((robot) -> {
robot.activity().createActivityWithComponentInNewTask();
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
index d91b38e..41102d6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraPolicyTest.java
@@ -20,7 +20,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -85,7 +85,7 @@
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraCompatFreeformPolicy_presentWhenEnabledAndDW() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -95,7 +95,7 @@
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraCompatFreeformPolicy_notPresentWhenNoDW() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(/* isAllowed= */ false);
@@ -105,7 +105,7 @@
}
@Test
- @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraCompatFreeformPolicy_notPresentWhenNoFlag() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -115,7 +115,7 @@
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraCompatFreeformPolicy_notPresentWhenNoFlagAndNoDW() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(/* isAllowed= */ false);
@@ -125,7 +125,7 @@
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraCompatFreeformPolicy_startedWhenEnabledAndDW() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(/* isAllowed= */ true);
@@ -136,7 +136,7 @@
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraStateManager_existsWhenCameraCompatFreeformExists() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(true);
@@ -147,7 +147,7 @@
}
@Test
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraStateManager_startedWhenCameraCompatFreeformExists() {
runTestScenario((robot) -> {
robot.allowEnterDesktopMode(true);
@@ -180,7 +180,7 @@
}
@Test
- @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testCameraStateManager_doesNotExistWhenNoPolicyExists() {
runTestScenario((robot) -> {
robot.conf().enableCameraCompatTreatmentAtBuildTime(/* enabled= */ false);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
index 21fac9b..d8373c5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatUtilsTest.java
@@ -16,10 +16,14 @@
package com.android.server.wm;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
+
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static org.mockito.Mockito.when;
+import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
+import android.app.TaskInfo;
import android.platform.test.annotations.Presubmit;
import androidx.annotation.NonNull;
@@ -114,6 +118,72 @@
});
}
+ @Test
+ public void testTopActivityEligibleForUserAspectRatioButton_eligible() {
+ runTestScenario((robot) -> {
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponentInNewTask();
+ a.setIgnoreOrientationRequest(true);
+ });
+ robot.conf().enableUserAppAspectRatioSettings(true);
+
+ robot.checkTaskInfoEligibleForUserAspectRatioButton(true);
+ });
+ }
+
+ @Test
+ public void testTopActivityEligibleForUserAspectRatioButton_disabled_notEligible() {
+ runTestScenario((robot) -> {
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponentInNewTask();
+ a.setIgnoreOrientationRequest(true);
+ });
+ robot.conf().enableUserAppAspectRatioSettings(false);
+
+ robot.checkTaskInfoEligibleForUserAspectRatioButton(false);
+ });
+ }
+
+ @Test
+ public void testTopActivityEligibleForUserAspectRatioButton_inSizeCompatMode_notEligible() {
+ runTestScenario((robot) -> {
+ robot.applyOnActivity((a) -> {
+ a.createActivityWithComponentInNewTask();
+ a.setIgnoreOrientationRequest(true);
+ a.setTopActivityOrganizedTask();
+ a.setTopActivityInSizeCompatMode(true);
+ a.setTopActivityVisible(true);
+ });
+ robot.conf().enableUserAppAspectRatioSettings(true);
+
+ robot.checkTaskInfoEligibleForUserAspectRatioButton(false);
+ });
+ }
+
+ @Test
+ public void testTopActivityEligibleForUserAspectRatioButton_transparentTop_notEligible() {
+ runTestScenario((robot) -> {
+ robot.transparentActivity((ta) -> {
+ ta.launchTransparentActivityInTask();
+ ta.activity().setIgnoreOrientationRequest(true);
+ });
+ robot.conf().enableUserAppAspectRatioSettings(true);
+
+ robot.checkTaskInfoEligibleForUserAspectRatioButton(false);
+ });
+ }
+
+ @Test
+ public void getTaskInfoPropagatesCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.applyOnActivity(AppCompatActivityRobot::createActivityWithComponentInNewTask);
+
+ robot.setFreeformCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+ robot.checkTaskInfoFreeformCameraCompatMode(
+ CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+ });
+ }
+
/**
* Runs a test scenario providing a Robot.
*/
@@ -125,11 +195,14 @@
private static class AppCompatUtilsRobotTest extends AppCompatRobotBase {
private final WindowState mWindowState;
+ @NonNull
+ private final AppCompatTransparentActivityRobot mTransparentActivityRobot;
AppCompatUtilsRobotTest(@NonNull WindowManagerService wm,
@NonNull ActivityTaskManagerService atm,
@NonNull ActivityTaskSupervisor supervisor) {
super(wm, atm, supervisor);
+ mTransparentActivityRobot = new AppCompatTransparentActivityRobot(activity());
mWindowState = Mockito.mock(WindowState.class);
}
@@ -139,6 +212,12 @@
spyOn(activity.mAppCompatController.getAppCompatAspectRatioPolicy());
}
+ void transparentActivity(@NonNull Consumer<AppCompatTransparentActivityRobot> consumer) {
+ // We always create at least an opaque activity in a Task.
+ activity().createNewTaskWithBaseActivity();
+ consumer.accept(mTransparentActivityRobot);
+ }
+
void setIsLetterboxedForFixedOrientationAndAspectRatio(
boolean forFixedOrientationAndAspectRatio) {
when(activity().top().mAppCompatController.getAppCompatAspectRatioPolicy()
@@ -155,11 +234,30 @@
when(mWindowState.isLetterboxedForDisplayCutout()).thenReturn(displayCutout);
}
+ void setFreeformCameraCompatMode(@FreeformCameraCompatMode int mode) {
+ activity().top().mAppCompatController.getAppCompatCameraOverrides()
+ .setFreeformCameraCompatMode(mode);
+ }
+
void checkTopActivityLetterboxReason(@NonNull String expected) {
Assert.assertEquals(expected,
AppCompatUtils.getLetterboxReasonString(activity().top(), mWindowState));
}
+ @NonNull
+ TaskInfo getTopTaskInfo() {
+ return activity().top().getTask().getTaskInfo();
+ }
+
+ void checkTaskInfoEligibleForUserAspectRatioButton(boolean eligible) {
+ Assert.assertEquals(eligible, getTopTaskInfo().appCompatTaskInfo
+ .eligibleForUserAspectRatioButton());
+ }
+
+ void checkTaskInfoFreeformCameraCompatMode(@FreeformCameraCompatMode int mode) {
+ Assert.assertEquals(mode, getTopTaskInfo().appCompatTaskInfo
+ .cameraCompatTaskInfo.freeformCameraCompatMode);
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index a48813d..dbcef10 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -35,7 +35,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -60,6 +60,7 @@
import android.graphics.Rect;
import android.hardware.camera2.CameraManager;
import android.os.Handler;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.DisplayInfo;
import android.view.Surface;
@@ -135,7 +136,6 @@
});
mActivityRefresher = new ActivityRefresher(mDisplayContent.mWmService, mMockHandler);
- mSetFlagsRule.enableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM);
CameraStateMonitor cameraStateMonitor =
new CameraStateMonitor(mDisplayContent, mMockHandler);
mCameraCompatFreeformPolicy =
@@ -147,6 +147,7 @@
cameraStateMonitor.startListeningToCameraState();
}
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void testFullscreen_doesNotActivateCameraCompatMode() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
@@ -157,6 +158,7 @@
assertNotInCameraCompatMode();
}
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void testOrientationUnspecified_doesNotActivateCameraCompatMode() {
configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
@@ -164,12 +166,14 @@
assertNotInCameraCompatMode();
}
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void testNoCameraConnection_doesNotActivateCameraCompatMode() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
assertNotInCameraCompatMode();
}
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -210,6 +214,7 @@
assertActivityRefreshRequested(/* refreshRequested */ false);
}
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -235,6 +240,7 @@
}
@Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -246,6 +252,7 @@
}
@Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -254,6 +261,7 @@
}
@Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
@@ -268,6 +276,7 @@
}
@Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
.thenReturn(false);
@@ -281,6 +290,7 @@
}
@Test
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
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 85cb1bc..5c0d424 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -83,7 +83,7 @@
import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
import static com.android.server.wm.WindowContainer.POSITION_TOP;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
-import static com.android.window.flags.Flags.FLAG_CAMERA_COMPAT_FOR_FREEFORM;
+import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
import static com.google.common.truth.Truth.assertThat;
@@ -2820,7 +2820,7 @@
verify(mWm.mUmInternal, never()).isUserVisible(userId2, displayId);
}
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void cameraCompatFreeformFlagEnabled_cameraCompatFreeformPolicyNotNull() {
doReturn(true).when(() ->
@@ -2829,7 +2829,7 @@
assertTrue(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
}
- @DisableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@Test
public void cameraCompatFreeformFlagNotEnabled_cameraCompatFreeformPolicyIsNull() {
doReturn(true).when(() ->
@@ -2838,7 +2838,7 @@
assertFalse(createNewDisplay().mAppCompatCameraPolicy.hasCameraCompatFreeformPolicy());
}
- @EnableFlags(FLAG_CAMERA_COMPAT_FOR_FREEFORM)
+ @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@Test
public void desktopWindowingFlagNotEnabled_cameraCompatFreeformPolicyIsNull() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index e0344d7..df17cd1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -432,6 +432,29 @@
}
@Test
+ public void testAddTaskCompatibleWindowingMode_withFreeformAndFullscreen_expectRemove() {
+ Task task1 = createTaskBuilder(".Task1")
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .build();
+ doReturn(WINDOWING_MODE_FREEFORM).when(task1).getWindowingMode();
+ mRecentTasks.add(task1);
+ mCallbacksRecorder.clear();
+
+ Task task2 = createTaskBuilder(".Task1")
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .setFlags(FLAG_ACTIVITY_NEW_TASK)
+ .build();
+ assertEquals(WINDOWING_MODE_FULLSCREEN, task2.getWindowingMode());
+ mRecentTasks.add(task2);
+
+ assertThat(mCallbacksRecorder.mAdded).hasSize(1);
+ assertThat(mCallbacksRecorder.mAdded).contains(task2);
+ assertThat(mCallbacksRecorder.mTrimmed).isEmpty();
+ assertThat(mCallbacksRecorder.mRemoved).hasSize(1);
+ assertThat(mCallbacksRecorder.mRemoved).contains(task1);
+ }
+
+ @Test
public void testAddTaskIncompatibleWindowingMode_expectNoRemove() {
Task task1 = createTaskBuilder(".Task1")
.setWindowingMode(WINDOWING_MODE_FULLSCREEN)
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 4b03483..e4512c3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -18,7 +18,6 @@
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
@@ -705,50 +704,6 @@
}
@Test
- public void testTopActivityEligibleForUserAspectRatioButton() {
- DisplayContent display = mAtm.mRootWindowContainer.getDefaultDisplay();
- final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
- .setWindowingMode(WINDOWING_MODE_FULLSCREEN).setDisplay(display).build();
- final Task task = rootTask.getBottomMostTask();
- final ActivityRecord root = task.getTopNonFinishingActivity();
- spyOn(mWm.mAppCompatConfiguration);
- spyOn(root);
- spyOn(root.mAppCompatController.getAppCompatAspectRatioOverrides());
-
- doReturn(true).when(root).fillsParent();
- doReturn(true).when(
- root.mAppCompatController.getAppCompatAspectRatioOverrides())
- .shouldEnableUserAspectRatioSettings();
- doReturn(false).when(root).inSizeCompatMode();
- doReturn(task).when(root).getOrganizedTask();
-
- // The button should be eligible to be displayed
- assertTrue(task.getTaskInfo()
- .appCompatTaskInfo.eligibleForUserAspectRatioButton());
-
- // When shouldApplyUserMinAspectRatioOverride is disable the button is not enabled
- doReturn(false).when(
- root.mAppCompatController.getAppCompatAspectRatioOverrides())
- .shouldEnableUserAspectRatioSettings();
- assertFalse(task.getTaskInfo()
- .appCompatTaskInfo.eligibleForUserAspectRatioButton());
- doReturn(true).when(root.mAppCompatController
- .getAppCompatAspectRatioOverrides()).shouldEnableUserAspectRatioSettings();
-
- // When in size compat mode the button is not enabled
- doReturn(true).when(root).inSizeCompatMode();
- assertFalse(task.getTaskInfo()
- .appCompatTaskInfo.eligibleForUserAspectRatioButton());
- doReturn(false).when(root).inSizeCompatMode();
-
- // When the top activity is transparent, the button is not enabled
- doReturn(false).when(root).fillsParent();
- assertFalse(task.getTaskInfo()
- .appCompatTaskInfo.eligibleForUserAspectRatioButton());
- doReturn(true).when(root).fillsParent();
- }
-
- @Test
public void testIsTopActivityTranslucent() {
DisplayContent display = mAtm.mRootWindowContainer.getDefaultDisplay();
final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
@@ -2112,17 +2067,6 @@
}
@Test
- public void getTaskInfoPropagatesCameraCompatMode() {
- final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
- final ActivityRecord activity = task.getTopMostActivity();
- activity.mAppCompatController.getAppCompatCameraOverrides().setFreeformCameraCompatMode(
- CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
-
- assertEquals(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE,
- task.getTaskInfo().appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode);
- }
-
- @Test
public void testUpdateTaskDescriptionOnReparent() {
final Task rootTask1 = createTask(mDisplayContent);
final Task rootTask2 = createTask(mDisplayContent);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
index eebb487..9e9874b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestWindowManagerPolicy.java
@@ -103,8 +103,8 @@
}
@Override
- public KeyEvent dispatchUnhandledKey(IBinder focusedToken, KeyEvent event, int policyFlags) {
- return null;
+ public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) {
+ return false;
}
@Override
diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
index 6c1e1a4..1294945 100644
--- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java
+++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java
@@ -1027,28 +1027,36 @@
boolean enabled = (mCurrentFunctions & UsbManager.FUNCTION_MIDI) != 0;
if (enabled != mMidiEnabled) {
if (enabled) {
+ boolean midiDeviceFound = false;
if (android.hardware.usb.flags.Flags.enableUsbSysfsMidiIdentification()) {
try {
getMidiCardDevice();
+ midiDeviceFound = true;
} catch (FileNotFoundException e) {
- Slog.e(TAG, "could not identify MIDI device", e);
- enabled = false;
+ Slog.w(TAG, "could not identify MIDI device", e);
}
- } else {
+ }
+ // For backward compatibility with older kernels without
+ // https://lore.kernel.org/r/20240307030922.3573161-1-royluo@google.com
+ if (!midiDeviceFound) {
Scanner scanner = null;
try {
scanner = new Scanner(new File(MIDI_ALSA_PATH));
mMidiCard = scanner.nextInt();
mMidiDevice = scanner.nextInt();
+ midiDeviceFound = true;
} catch (FileNotFoundException e) {
Slog.e(TAG, "could not open MIDI file", e);
- enabled = false;
} finally {
if (scanner != null) {
scanner.close();
}
}
}
+ if (!midiDeviceFound) {
+ Slog.e(TAG, "Failed to enable MIDI function");
+ enabled = false;
+ }
}
mMidiEnabled = enabled;
}
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index dc5f6e9..fb031bd 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -842,7 +842,7 @@
return;
}
- model.setRequested(config.allowMultipleTriggers);
+ model.setRequested(config.isAllowMultipleTriggers());
// TODO: Remove this block if the lower layer supports multiple triggers.
if (model.isRequested()) {
updateRecognitionLocked(model, true);
@@ -964,7 +964,7 @@
RecognitionConfig config = modelData.getRecognitionConfig();
if (config != null) {
// Whether we should continue by starting this again.
- modelData.setRequested(config.allowMultipleTriggers);
+ modelData.setRequested(config.isAllowMultipleTriggers());
}
// TODO: Remove this block if the lower layer supports multiple triggers.
if (modelData.isRequested()) {
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 862aff9..2bb86bc 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -1439,7 +1439,7 @@
runOrAddOperation(new Operation(
// always execute:
() -> {
- if (!mRecognitionConfig.allowMultipleTriggers) {
+ if (!mRecognitionConfig.isAllowMultipleTriggers()) {
// Unregister this remoteService once op is done
synchronized (mCallbacksLock) {
mCallbacks.remove(mPuuid.getUuid());
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index ba36007..4ae06a4 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -18,6 +18,8 @@
import android.content.Context
import android.content.ContextWrapper
+import android.content.pm.PackageManager
+import android.content.res.Resources
import android.hardware.input.IInputManager
import android.hardware.input.AidlKeyGestureEvent
import android.hardware.input.IKeyGestureEventListener
@@ -25,15 +27,20 @@
import android.hardware.input.InputManager
import android.hardware.input.InputManagerGlobal
import android.hardware.input.KeyGestureEvent
-import android.hardware.input.KeyGestureEvent.KeyGestureType
import android.os.IBinder
import android.os.Process
+import android.os.SystemClock
+import android.os.SystemProperties
import android.os.test.TestLooper
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.Presubmit
+import android.platform.test.flag.junit.SetFlagsRule
import android.view.InputDevice
-import android.view.KeyCharacterMap
import android.view.KeyEvent
+import android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE
import androidx.test.core.app.ApplicationProvider
+import com.android.internal.R
import com.android.internal.annotations.Keep
import com.android.internal.util.FrameworkStatsLog
import com.android.modules.utils.testing.ExtendedMockitoRule
@@ -78,32 +85,60 @@
KeyEvent.KEYCODE_META_LEFT to (KeyEvent.META_META_LEFT_ON or KeyEvent.META_META_ON),
KeyEvent.KEYCODE_META_RIGHT to (KeyEvent.META_META_RIGHT_ON or KeyEvent.META_META_ON),
)
+ const val SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH = 0
+ const val SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY = 1
+ const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0
+ const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1
+ const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2
}
@JvmField
@Rule
val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
- .mockStatic(FrameworkStatsLog::class.java).build()!!
+ .mockStatic(FrameworkStatsLog::class.java)
+ .mockStatic(SystemProperties::class.java).build()!!
+
+ @JvmField
+ @Rule
+ val rule = SetFlagsRule()
@Mock
private lateinit var iInputManager: IInputManager
+ @Mock
+ private lateinit var resources: Resources
+
+ @Mock
+ private lateinit var packageManager: PackageManager
+
private var currentPid = 0
- private lateinit var keyGestureController: KeyGestureController
private lateinit var context: Context
private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
private lateinit var testLooper: TestLooper
private var events = mutableListOf<KeyGestureEvent>()
- private var handleEvents = mutableListOf<KeyGestureEvent>()
@Before
fun setup() {
context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext()))
+ Mockito.`when`(context.resources).thenReturn(resources)
inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager)
setupInputDevices()
+ setupBehaviors()
testLooper = TestLooper()
currentPid = Process.myPid()
- keyGestureController = KeyGestureController(context, testLooper.looper)
+ }
+
+ private fun setupBehaviors() {
+ Mockito.`when`(
+ resources.getBoolean(
+ com.android.internal.R.bool.config_enableScreenshotChord
+ )
+ ).thenReturn(true)
+ Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_WATCH))
+ .thenReturn(true)
+ Mockito.`when`(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK))
+ .thenReturn(true)
+ Mockito.`when`(context.packageManager).thenReturn(packageManager)
}
private fun setupInputDevices() {
@@ -116,19 +151,22 @@
Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice)
}
- private fun notifyHomeGestureCompleted() {
- keyGestureController.notifyKeyGestureCompleted(DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H),
+ private fun notifyHomeGestureCompleted(keyGestureController: KeyGestureController) {
+ keyGestureController.notifyKeyGestureCompleted(
+ DEVICE_ID, intArrayOf(KeyEvent.KEYCODE_H),
KeyEvent.META_META_ON or KeyEvent.META_META_LEFT_ON,
- KeyGestureEvent.KEY_GESTURE_TYPE_HOME)
+ KeyGestureEvent.KEY_GESTURE_TYPE_HOME
+ )
}
@Test
fun testKeyGestureEvent_registerUnregisterListener() {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
val listener = KeyGestureEventListener()
// Register key gesture event listener
keyGestureController.registerKeyGestureEventListener(listener, 0)
- notifyHomeGestureCompleted()
+ notifyHomeGestureCompleted(keyGestureController)
testLooper.dispatchAll()
assertEquals(
"Listener should get callbacks on key gesture event completed",
@@ -144,7 +182,7 @@
// Unregister listener
events.clear()
keyGestureController.unregisterKeyGestureEventListener(listener, 0)
- notifyHomeGestureCompleted()
+ notifyHomeGestureCompleted(keyGestureController)
testLooper.dispatchAll()
assertEquals(
"Listener should not get callback after being unregistered",
@@ -155,20 +193,22 @@
@Test
fun testKeyGestureEvent_multipleGestureHandlers() {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+
// Set up two callbacks.
var callbackCount1 = 0
var callbackCount2 = 0
var selfCallback = 0
val externalHandler1 = KeyGestureHandler { _, _ ->
- callbackCount1++;
+ callbackCount1++
true
}
val externalHandler2 = KeyGestureHandler { _, _ ->
- callbackCount2++;
+ callbackCount2++
true
}
val selfHandler = KeyGestureHandler { _, _ ->
- selfCallback++;
+ selfCallback++
false
}
@@ -406,6 +446,14 @@
intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
),
TestData(
+ "META + / -> Open Shortcut Helper",
+ intArrayOf(KeyEvent.KEYCODE_META_LEFT, KeyEvent.KEYCODE_SLASH),
+ KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER,
+ intArrayOf(KeyEvent.KEYCODE_SLASH),
+ KeyEvent.META_META_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
+ TestData(
"BRIGHTNESS_UP -> Brightness Up",
intArrayOf(KeyEvent.KEYCODE_BRIGHTNESS_UP),
KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP,
@@ -528,12 +576,314 @@
KeyGestureEvent.ACTION_GESTURE_COMPLETE
)
),
+ TestData(
+ "CTRL + SPACE -> Switch Language Forward",
+ intArrayOf(KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_SPACE),
+ KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+ intArrayOf(KeyEvent.KEYCODE_SPACE),
+ KeyEvent.META_CTRL_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
+ TestData(
+ "CTRL + SHIFT + SPACE -> Switch Language Backward",
+ intArrayOf(
+ KeyEvent.KEYCODE_CTRL_LEFT,
+ KeyEvent.KEYCODE_SHIFT_LEFT,
+ KeyEvent.KEYCODE_SPACE
+ ),
+ KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH,
+ intArrayOf(KeyEvent.KEYCODE_SPACE),
+ KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
+ TestData(
+ "CTRL + ALT + Z -> Accessibility Shortcut",
+ intArrayOf(
+ KeyEvent.KEYCODE_CTRL_LEFT,
+ KeyEvent.KEYCODE_ALT_LEFT,
+ KeyEvent.KEYCODE_Z
+ ),
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
+ intArrayOf(KeyEvent.KEYCODE_Z),
+ KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
+ TestData(
+ "SYSRQ -> Take screenshot",
+ intArrayOf(KeyEvent.KEYCODE_SYSRQ),
+ KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT,
+ intArrayOf(KeyEvent.KEYCODE_SYSRQ),
+ 0,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
+ TestData(
+ "ESC -> Close All Dialogs",
+ intArrayOf(KeyEvent.KEYCODE_ESCAPE),
+ KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
+ intArrayOf(KeyEvent.KEYCODE_ESCAPE),
+ 0,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ ),
)
}
@Test
@Parameters(method = "keyGestureEventHandlerTestArguments")
fun testKeyGestures(test: TestData) {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(keyGestureController, test)
+ }
+
+ @Test
+ fun testKeycodesFullyConsumed_irrespectiveOfHandlers() {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ val testKeys = intArrayOf(
+ KeyEvent.KEYCODE_RECENT_APPS,
+ KeyEvent.KEYCODE_APP_SWITCH,
+ KeyEvent.KEYCODE_BRIGHTNESS_UP,
+ KeyEvent.KEYCODE_BRIGHTNESS_DOWN,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP,
+ KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE,
+ KeyEvent.KEYCODE_ALL_APPS,
+ KeyEvent.KEYCODE_NOTIFICATION,
+ KeyEvent.KEYCODE_SETTINGS,
+ KeyEvent.KEYCODE_LANGUAGE_SWITCH,
+ KeyEvent.KEYCODE_SCREENSHOT,
+ KeyEvent.KEYCODE_META_LEFT,
+ KeyEvent.KEYCODE_META_RIGHT,
+ KeyEvent.KEYCODE_ASSIST,
+ KeyEvent.KEYCODE_VOICE_ASSIST,
+ KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY,
+ KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY,
+ KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY,
+ KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL,
+ )
+
+ val handler = KeyGestureHandler { _, _ -> false }
+ keyGestureController.registerKeyGestureHandler(handler, 0)
+
+ for (key in testKeys) {
+ sendKeys(keyGestureController, intArrayOf(key), assertNotSentToApps = true)
+ }
+ }
+
+ @Test
+ fun testSearchKeyGestures_defaultSearch() {
+ Mockito.`when`(resources.getInteger(R.integer.config_searchKeyBehavior))
+ .thenReturn(SEARCH_KEY_BEHAVIOR_DEFAULT_SEARCH)
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureNotProduced(
+ keyGestureController,
+ "SEARCH -> Default Search",
+ intArrayOf(KeyEvent.KEYCODE_SEARCH),
+ )
+ }
+
+ @Test
+ fun testSearchKeyGestures_searchActivity() {
+ Mockito.`when`(resources.getInteger(R.integer.config_searchKeyBehavior))
+ .thenReturn(SEARCH_KEY_BEHAVIOR_TARGET_ACTIVITY)
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(
+ keyGestureController,
+ TestData(
+ "SEARCH -> Launch Search Activity",
+ intArrayOf(KeyEvent.KEYCODE_SEARCH),
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH,
+ intArrayOf(KeyEvent.KEYCODE_SEARCH),
+ 0,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ )
+ )
+ }
+
+ @Test
+ fun testSettingKeyGestures_doNothing() {
+ Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
+ .thenReturn(SETTINGS_KEY_BEHAVIOR_NOTHING)
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureNotProduced(
+ keyGestureController,
+ "SETTINGS -> Do Nothing",
+ intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ )
+ }
+
+ @Test
+ fun testSettingKeyGestures_settingsActivity() {
+ Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
+ .thenReturn(SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY)
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(
+ keyGestureController,
+ TestData(
+ "SETTINGS -> Launch Settings Activity",
+ intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS,
+ intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ 0,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ )
+ )
+ }
+
+ @Test
+ fun testSettingKeyGestures_notificationPanel() {
+ Mockito.`when`(resources.getInteger(R.integer.config_settingsKeyBehavior))
+ .thenReturn(SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL)
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(
+ keyGestureController,
+ TestData(
+ "SETTINGS -> Toggle Notification Panel",
+ intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL,
+ intArrayOf(KeyEvent.KEYCODE_SETTINGS),
+ 0,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ )
+ )
+ }
+
+ @Test
+ @EnableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+ fun testTriggerBugReport() {
+ Mockito.`when`(SystemProperties.get("ro.debuggable")).thenReturn("1")
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(
+ keyGestureController,
+ TestData(
+ "META + CTRL + DEL -> Trigger Bug Report",
+ intArrayOf(
+ KeyEvent.KEYCODE_META_LEFT,
+ KeyEvent.KEYCODE_CTRL_LEFT,
+ KeyEvent.KEYCODE_DEL
+ ),
+ KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT,
+ intArrayOf(KeyEvent.KEYCODE_DEL),
+ KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ )
+ )
+ }
+
+ @Test
+ @DisableFlags(com.android.server.flags.Flags.FLAG_NEW_BUGREPORT_KEYBOARD_SHORTCUT)
+ fun testTriggerBugReport_flagDisabled() {
+ Mockito.`when`(SystemProperties.get("ro.debuggable")).thenReturn("1")
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(
+ keyGestureController,
+ TestData(
+ "META + CTRL + DEL -> Not Trigger Bug Report (Fallback to BACK)",
+ intArrayOf(
+ KeyEvent.KEYCODE_META_LEFT,
+ KeyEvent.KEYCODE_CTRL_LEFT,
+ KeyEvent.KEYCODE_DEL
+ ),
+ KeyGestureEvent.KEY_GESTURE_TYPE_BACK,
+ intArrayOf(KeyEvent.KEYCODE_DEL),
+ KeyEvent.META_META_ON,
+ intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
+ )
+ )
+ }
+
+ @Test
+ fun testCapsLockPressNotified() {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ val listener = KeyGestureEventListener()
+
+ keyGestureController.registerKeyGestureEventListener(listener, 0)
+ sendKeys(keyGestureController, intArrayOf(KeyEvent.KEYCODE_CAPS_LOCK))
+ testLooper.dispatchAll()
+ assertEquals(
+ "Listener should get callbacks on key gesture event completed",
+ 1,
+ events.size
+ )
+ assertEquals(
+ "Listener should get callback for Toggle Caps Lock key gesture complete event",
+ KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK,
+ events[0].keyGestureType
+ )
+ }
+
+ @Keep
+ private fun keyGestureEventHandlerTestArguments_forKeyCombinations(): Array<TestData> {
+ return arrayOf(
+ TestData(
+ "VOLUME_DOWN + POWER -> Screenshot Chord",
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER),
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER),
+ 0,
+ intArrayOf(
+ KeyGestureEvent.ACTION_GESTURE_START,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE
+ )
+ ),
+ TestData(
+ "POWER + STEM_PRIMARY -> Screenshot Chord",
+ intArrayOf(KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_STEM_PRIMARY),
+ KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD,
+ intArrayOf(KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_STEM_PRIMARY),
+ 0,
+ intArrayOf(
+ KeyGestureEvent.ACTION_GESTURE_START,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE
+ )
+ ),
+ TestData(
+ "VOLUME_DOWN + VOLUME_UP -> Accessibility Chord",
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP),
+ 0,
+ intArrayOf(
+ KeyGestureEvent.ACTION_GESTURE_START,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE
+ )
+ ),
+ TestData(
+ "BACK + DPAD_DOWN -> TV Accessibility Chord",
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+ 0,
+ intArrayOf(
+ KeyGestureEvent.ACTION_GESTURE_START,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE
+ )
+ ),
+ TestData(
+ "BACK + DPAD_CENTER -> TV Trigger Bug Report",
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER),
+ KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER),
+ 0,
+ intArrayOf(
+ KeyGestureEvent.ACTION_GESTURE_START,
+ KeyGestureEvent.ACTION_GESTURE_COMPLETE
+ )
+ ),
+ )
+ }
+
+ @Test
+ @Parameters(method = "keyGestureEventHandlerTestArguments_forKeyCombinations")
+ @EnableFlags(
+ com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER,
+ com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER_MULTI_PRESS_GESTURES
+ )
+ fun testKeyCombinationGestures(test: TestData) {
+ val keyGestureController = KeyGestureController(context, testLooper.looper)
+ testKeyGestureInternal(keyGestureController, test)
+ }
+
+ private fun testKeyGestureInternal(keyGestureController: KeyGestureController, test: TestData) {
+ var handleEvents = mutableListOf<KeyGestureEvent>()
val handler = KeyGestureHandler { event, _ ->
handleEvents.add(KeyGestureEvent(event))
true
@@ -541,7 +891,7 @@
keyGestureController.registerKeyGestureHandler(handler, 0)
handleEvents.clear()
- sendKeys(test.keys, /* assertAllConsumed = */ false)
+ sendKeys(keyGestureController, test.keys)
assertEquals(
"Test: $test doesn't produce correct number of key gesture events",
@@ -575,55 +925,37 @@
keyGestureController.unregisterKeyGestureHandler(handler, 0)
}
- @Test
- fun testKeycodesFullyConsumed_irrespectiveOfHandlers() {
- val testKeys = intArrayOf(
- KeyEvent.KEYCODE_RECENT_APPS,
- KeyEvent.KEYCODE_APP_SWITCH,
- KeyEvent.KEYCODE_BRIGHTNESS_UP,
- KeyEvent.KEYCODE_BRIGHTNESS_DOWN,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP,
- KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE,
- KeyEvent.KEYCODE_ALL_APPS,
- KeyEvent.KEYCODE_NOTIFICATION,
- KeyEvent.KEYCODE_SETTINGS,
- KeyEvent.KEYCODE_LANGUAGE_SWITCH,
- KeyEvent.KEYCODE_SCREENSHOT,
- KeyEvent.KEYCODE_META_LEFT,
- KeyEvent.KEYCODE_META_RIGHT,
- KeyEvent.KEYCODE_ASSIST,
- KeyEvent.KEYCODE_VOICE_ASSIST,
- KeyEvent.KEYCODE_STYLUS_BUTTON_PRIMARY,
- KeyEvent.KEYCODE_STYLUS_BUTTON_SECONDARY,
- KeyEvent.KEYCODE_STYLUS_BUTTON_TERTIARY,
- KeyEvent.KEYCODE_STYLUS_BUTTON_TAIL,
- )
-
- val handler = KeyGestureHandler { _, _ -> false }
- keyGestureController.registerKeyGestureHandler(handler, 0)
-
- for (key in testKeys) {
- sendKeys(intArrayOf(key), /* assertAllConsumed = */ true)
+ private fun testKeyGestureNotProduced(
+ keyGestureController: KeyGestureController,
+ testName: String,
+ testKeys: IntArray
+ ) {
+ var handleEvents = mutableListOf<KeyGestureEvent>()
+ val handler = KeyGestureHandler { event, _ ->
+ handleEvents.add(KeyGestureEvent(event))
+ true
}
+ keyGestureController.registerKeyGestureHandler(handler, 0)
+ handleEvents.clear()
+
+ sendKeys(keyGestureController, testKeys)
+ assertEquals("Test: $testName should not produce Key gesture", 0, handleEvents.size)
}
- private fun sendKeys(testKeys: IntArray, assertAllConsumed: Boolean) {
+ private fun sendKeys(
+ keyGestureController: KeyGestureController,
+ testKeys: IntArray,
+ assertNotSentToApps: Boolean = false
+ ) {
var metaState = 0
+ val now = SystemClock.uptimeMillis()
for (key in testKeys) {
val downEvent = KeyEvent(
- /* downTime = */0, /* eventTime = */ 0, KeyEvent.ACTION_DOWN, key,
- 0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
- 0 /*flags*/, InputDevice.SOURCE_KEYBOARD
+ now, now, KeyEvent.ACTION_DOWN, key, 0 /*repeat*/, metaState,
+ DEVICE_ID, 0 /*scancode*/, 0 /*flags*/,
+ InputDevice.SOURCE_KEYBOARD
)
- val consumed =
- keyGestureController.interceptKeyBeforeDispatching(null, downEvent, 0) == -1L
- if (assertAllConsumed) {
- assertTrue(
- "interceptKeyBeforeDispatching should consume all events $downEvent",
- consumed
- )
- }
+ interceptKey(keyGestureController, downEvent, assertNotSentToApps)
metaState = metaState or MODIFIER.getOrDefault(key, 0)
downEvent.recycle()
@@ -632,24 +964,39 @@
for (key in testKeys.reversed()) {
val upEvent = KeyEvent(
- /* downTime = */0, /* eventTime = */ 0, KeyEvent.ACTION_UP, key,
- 0 /*repeat*/, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/,
- 0 /*flags*/, InputDevice.SOURCE_KEYBOARD
+ now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState,
+ DEVICE_ID, 0 /*scancode*/, 0 /*flags*/,
+ InputDevice.SOURCE_KEYBOARD
)
- val consumed =
- keyGestureController.interceptKeyBeforeDispatching(null, upEvent, 0) == -1L
- if (assertAllConsumed) {
- assertTrue(
- "interceptKeyBeforeDispatching should consume all events $upEvent",
- consumed
- )
- }
+ interceptKey(keyGestureController, upEvent, assertNotSentToApps)
+ metaState = metaState and MODIFIER.getOrDefault(key, 0).inv()
upEvent.recycle()
testLooper.dispatchAll()
}
}
+ private fun interceptKey(
+ keyGestureController: KeyGestureController,
+ event: KeyEvent,
+ assertNotSentToApps: Boolean
+ ) {
+ keyGestureController.interceptKeyBeforeQueueing(event, FLAG_INTERACTIVE)
+ testLooper.dispatchAll()
+
+ val consumed =
+ keyGestureController.interceptKeyBeforeDispatching(null, event, 0) == -1L
+ if (assertNotSentToApps) {
+ assertTrue(
+ "interceptKeyBeforeDispatching should consume all events $event",
+ consumed
+ )
+ }
+ if (!consumed) {
+ keyGestureController.interceptUnhandledKey(event, null)
+ }
+ }
+
inner class KeyGestureEventListener : IKeyGestureEventListener.Stub() {
override fun onKeyGestureEvent(event: AidlKeyGestureEvent) {
events.add(KeyGestureEvent(event))
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index b5258df..60fa52f 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -402,4 +402,73 @@
// Verify that no updateViewLayout is called (as expected for a two-finger drag gesture).
verify(mWindowManager, times(0)).updateViewLayout(any(), any());
}
-}
\ No newline at end of file
+
+ @Test
+ public void testPinchDrag() {
+ float offsetY = ViewConfiguration.get(mTestableContext).getScaledTouchSlop() + 10;
+
+ MotionEvent actionDown = new MotionEventBuilder(MotionEvent.ACTION_DOWN, SOURCE_MOUSE)
+ .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(40f)
+ )
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build();
+ mTouchpadDebugView.dispatchTouchEvent(actionDown);
+
+ MotionEvent pointerDown = new MotionEventBuilder(MotionEvent.ACTION_POINTER_DOWN,
+ SOURCE_MOUSE)
+ .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(40f)
+ )
+ .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(45f)
+ )
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build();
+ mTouchpadDebugView.dispatchTouchEvent(pointerDown);
+
+ // Simulate ACTION_MOVE event (both fingers moving apart).
+ MotionEvent actionMove = new MotionEventBuilder(MotionEvent.ACTION_MOVE, SOURCE_MOUSE)
+ .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(40f - offsetY)
+ )
+ .rawXCursorPosition(mWindowLayoutParams.x + 10f)
+ .rawYCursorPosition(mWindowLayoutParams.y + 10f)
+ .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(45f + offsetY)
+ )
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build();
+ mTouchpadDebugView.dispatchTouchEvent(actionMove);
+
+ MotionEvent pointerUp = new MotionEventBuilder(MotionEvent.ACTION_POINTER_UP, SOURCE_MOUSE)
+ .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(40f - offsetY)
+ )
+ .pointer(new PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(45f + offsetY)
+ )
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build();
+ mTouchpadDebugView.dispatchTouchEvent(pointerUp);
+
+ MotionEvent actionUp = new MotionEventBuilder(MotionEvent.ACTION_UP, SOURCE_MOUSE)
+ .pointer(new PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER)
+ .x(40f)
+ .y(40f - offsetY)
+ )
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build();
+ mTouchpadDebugView.dispatchTouchEvent(actionUp);
+
+ // Verify that no updateViewLayout is called (as expected for a two-finger drag gesture).
+ verify(mWindowManager, times(0)).updateViewLayout(any(), any());
+ }
+}
diff --git a/tests/Tracing/TEST_MAPPING b/tests/Tracing/TEST_MAPPING
index 7f58fce..f6e5221 100644
--- a/tests/Tracing/TEST_MAPPING
+++ b/tests/Tracing/TEST_MAPPING
@@ -1,5 +1,5 @@
{
- "postsubmit": [
+ "presubmit": [
{
"name": "TracingTests"
}
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp
index df1d51e..064b461 100644
--- a/tools/aapt2/Debug.cpp
+++ b/tools/aapt2/Debug.cpp
@@ -346,6 +346,21 @@
value->value->Accept(&body_printer);
printer->Undent();
}
+ printer->Println("Flag disabled values:");
+ for (const auto& value : entry.flag_disabled_values) {
+ printer->Print("(");
+ printer->Print(value->config.to_string());
+ printer->Print(") ");
+ value->value->Accept(&headline_printer);
+ if (options.show_sources && !value->value->GetSource().path.empty()) {
+ printer->Print(" src=");
+ printer->Print(value->value->GetSource().to_string());
+ }
+ printer->Println();
+ printer->Indent();
+ value->value->Accept(&body_printer);
+ printer->Undent();
+ }
printer->Undent();
}
}
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h
index a274f04..0d261ab 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -71,6 +71,17 @@
enum class FlagStatus { NoFlag = 0, Disabled = 1, Enabled = 2 };
+struct FeatureFlagAttribute {
+ std::string name;
+ bool negated = false;
+
+ std::string ToString() {
+ return (negated ? "!" : "") + name;
+ }
+
+ bool operator==(const FeatureFlagAttribute& o) const = default;
+};
+
android::StringPiece to_string(ResourceType type);
/**
@@ -232,6 +243,12 @@
// Exported symbols
std::vector<SourcedResourceName> exported_symbols;
+
+ // Flag status
+ FlagStatus flag_status = FlagStatus::NoFlag;
+
+ // Flag
+ std::optional<FeatureFlagAttribute> flag;
};
/**
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index a5aecc8..fce6aa7 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -107,9 +107,10 @@
Visibility::Level visibility_level = Visibility::Level::kUndefined;
bool staged_api = false;
bool allow_new = false;
- FlagStatus flag_status = FlagStatus::NoFlag;
std::optional<OverlayableItem> overlayable_item;
std::optional<StagedId> staged_alias;
+ std::optional<FeatureFlagAttribute> flag;
+ FlagStatus flag_status;
std::string comment;
std::unique_ptr<Value> value;
@@ -151,6 +152,7 @@
}
if (res->value != nullptr) {
+ res->value->SetFlag(res->flag);
res->value->SetFlagStatus(res->flag_status);
// Attach the comment, source and config to the value.
res->value->SetComment(std::move(res->comment));
@@ -162,8 +164,6 @@
res_builder.SetStagedId(res->staged_alias.value());
}
- res_builder.SetFlagStatus(res->flag_status);
-
bool error = false;
if (!res->name.entry.empty()) {
if (!table->AddResource(res_builder.Build(), diag)) {
@@ -546,12 +546,26 @@
{"symbol", std::mem_fn(&ResourceParser::ParseSymbol)},
});
- std::string resource_type = parser->element_name();
- auto flag_status = GetFlagStatus(parser);
- if (!flag_status) {
- return false;
+ std::string_view resource_type = parser->element_name();
+ if (auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag"))) {
+ if (options_.flag) {
+ diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
+ << "Resource flag are not allowed both in the path and in the file");
+ return false;
+ }
+ out_resource->flag = std::move(flag);
+ std::string error;
+ auto flag_status = GetFlagStatus(out_resource->flag, options_.feature_flag_values, &error);
+ if (flag_status) {
+ out_resource->flag_status = flag_status.value();
+ } else {
+ diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number())) << error);
+ return false;
+ }
+ } else if (options_.flag) {
+ out_resource->flag = options_.flag;
+ out_resource->flag_status = options_.flag_status;
}
- out_resource->flag_status = flag_status.value();
// The value format accepted for this resource.
uint32_t resource_format = 0u;
@@ -567,7 +581,7 @@
// Items have their type encoded in the type attribute.
if (std::optional<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type")) {
- resource_type = std::string(maybe_type.value());
+ resource_type = maybe_type.value();
} else {
diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
<< "<item> must have a 'type' attribute");
@@ -590,7 +604,7 @@
// Bags have their type encoded in the type attribute.
if (std::optional<StringPiece> maybe_type = xml::FindNonEmptyAttribute(parser, "type")) {
- resource_type = std::string(maybe_type.value());
+ resource_type = maybe_type.value();
} else {
diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
<< "<bag> must have a 'type' attribute");
@@ -733,33 +747,6 @@
return false;
}
-std::optional<FlagStatus> ResourceParser::GetFlagStatus(xml::XmlPullParser* parser) {
- auto flag_status = FlagStatus::NoFlag;
-
- std::optional<StringPiece> flag = xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag");
- if (flag) {
- auto flag_it = options_.feature_flag_values.find(flag.value());
- if (flag_it == options_.feature_flag_values.end()) {
- diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
- << "Resource flag value undefined");
- return {};
- }
- const auto& flag_properties = flag_it->second;
- if (!flag_properties.read_only) {
- diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
- << "Only read only flags may be used with resources");
- return {};
- }
- if (!flag_properties.enabled.has_value()) {
- diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
- << "Only flags with a value may be used with resources");
- return {};
- }
- flag_status = flag_properties.enabled.value() ? FlagStatus::Enabled : FlagStatus::Disabled;
- }
- return flag_status;
-}
-
bool ResourceParser::ParseItem(xml::XmlPullParser* parser,
ParsedResource* out_resource,
const uint32_t format) {
@@ -1666,21 +1653,25 @@
const std::string& element_namespace = parser->element_namespace();
const std::string& element_name = parser->element_name();
if (element_namespace.empty() && element_name == "item") {
- auto flag_status = GetFlagStatus(parser);
- if (!flag_status) {
- error = true;
- continue;
- }
+ auto flag = ParseFlag(xml::FindAttribute(parser, xml::kSchemaAndroid, "featureFlag"));
std::unique_ptr<Item> item = ParseXml(parser, typeMask, kNoRawString);
if (!item) {
diag_->Error(android::DiagMessage(item_source) << "could not parse array item");
error = true;
continue;
}
- item->SetFlagStatus(flag_status.value());
+ item->SetFlag(flag);
+ std::string err;
+ auto status = GetFlagStatus(flag, options_.feature_flag_values, &err);
+ if (status) {
+ item->SetFlagStatus(status.value());
+ } else {
+ diag_->Error(android::DiagMessage(item_source) << err);
+ error = true;
+ continue;
+ }
item->SetSource(item_source);
array->elements.emplace_back(std::move(item));
-
} else if (!ShouldIgnoreElement(element_namespace, element_name)) {
diag_->Error(android::DiagMessage(source_.WithLine(parser->line_number()))
<< "unknown tag <" << element_namespace << ":" << element_name << ">");
diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h
index 442dea8..90690d5 100644
--- a/tools/aapt2/ResourceParser.h
+++ b/tools/aapt2/ResourceParser.h
@@ -57,6 +57,11 @@
std::optional<Visibility::Level> visibility;
FeatureFlagValues feature_flag_values;
+
+ // The flag that should be applied to all resources parsed
+ std::optional<FeatureFlagAttribute> flag;
+
+ FlagStatus flag_status = FlagStatus::NoFlag;
};
struct FlattenedXmlSubTree {
@@ -85,8 +90,6 @@
private:
DISALLOW_COPY_AND_ASSIGN(ResourceParser);
- std::optional<FlagStatus> GetFlagStatus(xml::XmlPullParser* parser);
-
std::optional<FlattenedXmlSubTree> CreateFlattenSubTree(xml::XmlPullParser* parser);
// Parses the XML subtree as a StyleString (flattened XML representation for strings with
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index 9751459..5435cba2 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -101,6 +101,21 @@
}
};
+struct ConfigFlagKey {
+ const ConfigDescription* config;
+ StringPiece product;
+ const FeatureFlagAttribute& flag;
+};
+
+struct lt_config_flag_key_ref {
+ template <typename T>
+ bool operator()(const T& lhs, const ConfigFlagKey& rhs) const noexcept {
+ return std::tie(lhs->config, lhs->product, lhs->value->GetFlag()->name,
+ lhs->value->GetFlag()->negated) <
+ std::tie(*rhs.config, rhs.product, rhs.flag.name, rhs.flag.negated);
+ }
+};
+
} // namespace
ResourceTable::ResourceTable(ResourceTable::Validation validation) : validation_(validation) {
@@ -213,6 +228,25 @@
return results;
}
+ResourceConfigValue* ResourceEntry::FindOrCreateFlagDisabledValue(
+ const FeatureFlagAttribute& flag, const android::ConfigDescription& config,
+ android::StringPiece product) {
+ auto iter = std::lower_bound(flag_disabled_values.begin(), flag_disabled_values.end(),
+ ConfigFlagKey{&config, product, flag}, lt_config_flag_key_ref());
+ if (iter != flag_disabled_values.end()) {
+ ResourceConfigValue* value = iter->get();
+ const auto value_flag = value->value->GetFlag().value();
+ if (value_flag.name == flag.name && value_flag.negated == flag.negated &&
+ value->config == config && value->product == product) {
+ return value;
+ }
+ }
+ ResourceConfigValue* newValue =
+ flag_disabled_values.insert(iter, util::make_unique<ResourceConfigValue>(config, product))
+ ->get();
+ return newValue;
+}
+
bool ResourceEntry::HasDefaultValue() const {
// The default config should be at the top of the list, since the list is sorted.
return !values.empty() && values.front()->config == ConfigDescription::DefaultConfig();
@@ -375,13 +409,14 @@
}
};
-void InsertEntryIntoTableView(ResourceTableView& table, const ResourceTablePackage* package,
- const ResourceTableType* type, const std::string& entry_name,
- const std::optional<ResourceId>& id, const Visibility& visibility,
- const std::optional<AllowNew>& allow_new,
- const std::optional<OverlayableItem>& overlayable_item,
- const std::optional<StagedId>& staged_id,
- const std::vector<std::unique_ptr<ResourceConfigValue>>& values) {
+void InsertEntryIntoTableView(
+ ResourceTableView& table, const ResourceTablePackage* package, const ResourceTableType* type,
+ const std::string& entry_name, const std::optional<ResourceId>& id,
+ const Visibility& visibility, const std::optional<AllowNew>& allow_new,
+ const std::optional<OverlayableItem>& overlayable_item,
+ const std::optional<StagedId>& staged_id,
+ const std::vector<std::unique_ptr<ResourceConfigValue>>& values,
+ const std::vector<std::unique_ptr<ResourceConfigValue>>& flag_disabled_values) {
SortedVectorInserter<ResourceTablePackageView, PackageViewComparer> package_inserter;
SortedVectorInserter<ResourceTableTypeView, TypeViewComparer> type_inserter;
SortedVectorInserter<ResourceTableEntryView, EntryViewComparer> entry_inserter;
@@ -408,6 +443,9 @@
for (auto& value : values) {
new_entry.values.emplace_back(value.get());
}
+ for (auto& value : flag_disabled_values) {
+ new_entry.flag_disabled_values.emplace_back(value.get());
+ }
entry_inserter.Insert(view_type->entries, std::move(new_entry));
}
@@ -426,6 +464,21 @@
return nullptr;
}
+const ResourceConfigValue* ResourceTableEntryView::FindFlagDisabledValue(
+ const FeatureFlagAttribute& flag, const ConfigDescription& config,
+ android::StringPiece product) const {
+ auto iter = std::lower_bound(flag_disabled_values.begin(), flag_disabled_values.end(),
+ ConfigFlagKey{&config, product, flag}, lt_config_flag_key_ref());
+ if (iter != values.end()) {
+ const ResourceConfigValue* value = *iter;
+ if (value->value->GetFlag() == flag && value->config == config &&
+ StringPiece(value->product) == product) {
+ return value;
+ }
+ }
+ return nullptr;
+}
+
ResourceTableView ResourceTable::GetPartitionedView(const ResourceTableViewOptions& options) const {
ResourceTableView view;
for (const auto& package : packages) {
@@ -433,13 +486,13 @@
for (const auto& entry : type->entries) {
InsertEntryIntoTableView(view, package.get(), type.get(), entry->name, entry->id,
entry->visibility, entry->allow_new, entry->overlayable_item,
- entry->staged_id, entry->values);
+ entry->staged_id, entry->values, entry->flag_disabled_values);
if (options.create_alias_entries && entry->staged_id) {
auto alias_id = entry->staged_id.value().id;
InsertEntryIntoTableView(view, package.get(), type.get(), entry->name, alias_id,
entry->visibility, entry->allow_new, entry->overlayable_item, {},
- entry->values);
+ entry->values, entry->flag_disabled_values);
}
}
}
@@ -587,6 +640,25 @@
entry->staged_id = res.staged_id.value();
}
+ if (res.value != nullptr && res.value->GetFlagStatus() == FlagStatus::Disabled) {
+ auto disabled_config_value =
+ entry->FindOrCreateFlagDisabledValue(res.value->GetFlag().value(), res.config, res.product);
+ if (!disabled_config_value->value) {
+ // Resource does not exist, add it now.
+ // Must clone the value since it might be in the values vector as well
+ CloningValueTransformer cloner(&string_pool);
+ disabled_config_value->value = res.value->Transform(cloner);
+ } else {
+ diag->Error(android::DiagMessage(source)
+ << "duplicate value for resource '" << res.name << "' " << "with config '"
+ << res.config << "' and flag '"
+ << (res.value->GetFlag().value().negated ? "!" : "")
+ << res.value->GetFlag().value().name << "'");
+ diag->Error(android::DiagMessage(source) << "resource previously defined here");
+ return false;
+ }
+ }
+
if (res.value != nullptr) {
auto config_value = entry->FindOrCreateValue(res.config, res.product);
if (!config_value->value) {
@@ -595,9 +667,9 @@
} else {
// When validation is enabled, ensure that a resource cannot have multiple values defined for
// the same configuration unless protected by flags.
- auto result =
- validate ? ResolveFlagCollision(config_value->value->GetFlagStatus(), res.flag_status)
- : CollisionResult::kKeepBoth;
+ auto result = validate ? ResolveFlagCollision(config_value->value->GetFlagStatus(),
+ res.value->GetFlagStatus())
+ : CollisionResult::kKeepBoth;
if (result == CollisionResult::kConflict) {
result = ResolveValueCollision(config_value->value.get(), res.value.get());
}
@@ -771,11 +843,6 @@
return *this;
}
-NewResourceBuilder& NewResourceBuilder::SetFlagStatus(FlagStatus flag_status) {
- res_.flag_status = flag_status;
- return *this;
-}
-
NewResource NewResourceBuilder::Build() {
return std::move(res_);
}
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index cba6b70..b0e1855 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -136,6 +136,9 @@
// The resource's values for each configuration.
std::vector<std::unique_ptr<ResourceConfigValue>> values;
+ // The resource's values that are behind disabled flags.
+ std::vector<std::unique_ptr<ResourceConfigValue>> flag_disabled_values;
+
explicit ResourceEntry(android::StringPiece name) : name(name) {
}
@@ -148,6 +151,13 @@
android::StringPiece product);
std::vector<ResourceConfigValue*> FindAllValues(const android::ConfigDescription& config);
+ // Either returns the existing ResourceConfigValue in the disabled list with the given flag,
+ // config, and product or creates a new one and returns that. In either case the returned value
+ // does not have the flag set on the value so it must be set by the caller.
+ ResourceConfigValue* FindOrCreateFlagDisabledValue(const FeatureFlagAttribute& flag,
+ const android::ConfigDescription& config,
+ android::StringPiece product = {});
+
template <typename Func>
std::vector<ResourceConfigValue*> FindValuesIf(Func f) {
std::vector<ResourceConfigValue*> results;
@@ -215,9 +225,14 @@
std::optional<OverlayableItem> overlayable_item;
std::optional<StagedId> staged_id;
std::vector<const ResourceConfigValue*> values;
+ std::vector<const ResourceConfigValue*> flag_disabled_values;
const ResourceConfigValue* FindValue(const android::ConfigDescription& config,
android::StringPiece product = {}) const;
+
+ const ResourceConfigValue* FindFlagDisabledValue(const FeatureFlagAttribute& flag,
+ const android::ConfigDescription& config,
+ android::StringPiece product = {}) const;
};
struct ResourceTableTypeView {
@@ -269,7 +284,6 @@
std::optional<AllowNew> allow_new;
std::optional<StagedId> staged_id;
bool allow_mangled = false;
- FlagStatus flag_status = FlagStatus::NoFlag;
};
struct NewResourceBuilder {
@@ -283,7 +297,6 @@
NewResourceBuilder& SetAllowNew(AllowNew allow_new);
NewResourceBuilder& SetStagedId(StagedId id);
NewResourceBuilder& SetAllowMangled(bool allow_mangled);
- NewResourceBuilder& SetFlagStatus(FlagStatus flag_status);
NewResource Build();
private:
diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp
index b75e87c..723cfc0 100644
--- a/tools/aapt2/ResourceValues.cpp
+++ b/tools/aapt2/ResourceValues.cpp
@@ -1102,6 +1102,7 @@
std::unique_ptr<T> CopyValueFields(std::unique_ptr<T> new_value, const T* value) {
new_value->SetSource(value->GetSource());
new_value->SetComment(value->GetComment());
+ new_value->SetFlag(value->GetFlag());
new_value->SetFlagStatus(value->GetFlagStatus());
return new_value;
}
diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h
index a1b1839..e000c65 100644
--- a/tools/aapt2/ResourceValues.h
+++ b/tools/aapt2/ResourceValues.h
@@ -65,10 +65,21 @@
return translatable_;
}
+ void SetFlag(std::optional<FeatureFlagAttribute> val) {
+ flag_ = val;
+ }
+
+ std::optional<FeatureFlagAttribute> GetFlag() const {
+ return flag_;
+ }
+
void SetFlagStatus(FlagStatus val) {
flag_status_ = val;
}
+ // If the value is behind a flag this returns whether that flag was enabled when the value was
+ // parsed by comparing it to the flags passed on the command line to aapt2 (taking into account
+ // negation if necessary). If there was no flag, FlagStatus::NoFlag is returned instead.
FlagStatus GetFlagStatus() const {
return flag_status_;
}
@@ -128,6 +139,7 @@
std::string comment_;
bool weak_ = false;
bool translatable_ = true;
+ std::optional<FeatureFlagAttribute> flag_;
FlagStatus flag_status_ = FlagStatus::NoFlag;
private:
diff --git a/tools/aapt2/Resources.proto b/tools/aapt2/Resources.proto
index 5c64089..a0f60b6 100644
--- a/tools/aapt2/Resources.proto
+++ b/tools/aapt2/Resources.proto
@@ -240,6 +240,9 @@
// The staged resource ID of this finalized resource.
StagedId staged_id = 7;
+
+ // The set of values defined for this entry which are behind disabled flags
+ repeated ConfigValue flag_disabled_config_value = 8;
}
// A Configuration/Value pair.
@@ -283,6 +286,8 @@
// The status of the flag the value is behind if any
uint32 flag_status = 8;
+ bool flag_negated = 9;
+ string flag_name = 10;
}
// A CompoundValue is an abstract type. It represents a value that is a made of other values.
diff --git a/tools/aapt2/ResourcesInternal.proto b/tools/aapt2/ResourcesInternal.proto
index b0ed3da3..f4735a2 100644
--- a/tools/aapt2/ResourcesInternal.proto
+++ b/tools/aapt2/ResourcesInternal.proto
@@ -49,4 +49,9 @@
// Any symbols this file auto-generates/exports (eg. @+id/foo in an XML file).
repeated Symbol exported_symbol = 5;
+
+ // The status of the flag the file is behind if any
+ uint32 flag_status = 6;
+ bool flag_negated = 7;
+ string flag_name = 8;
}
diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index 2a978a5..52372fa 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -67,6 +67,7 @@
std::string resource_dir;
std::string name;
std::string extension;
+ std::string flag_name;
// Original config str. We keep this because when we parse the config, we may add on
// version qualifiers. We want to preserve the original input so the output is easily
@@ -81,6 +82,22 @@
std::string* out_error,
const CompileOptions& options) {
std::vector<std::string> parts = util::Split(path, dir_sep);
+
+ std::string flag_name;
+ // Check for a flag
+ for (auto iter = parts.begin(); iter != parts.end();) {
+ if (iter->starts_with("flag(") && iter->ends_with(")")) {
+ if (!flag_name.empty()) {
+ if (out_error) *out_error = "resource path cannot contain more than one flag directory";
+ return {};
+ }
+ flag_name = iter->substr(5, iter->size() - 6);
+ iter = parts.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+
if (parts.size() < 2) {
if (out_error) *out_error = "bad resource path";
return {};
@@ -131,6 +148,7 @@
std::string(dir_str),
std::string(name),
std::string(extension),
+ std::move(flag_name),
std::string(config_str),
config};
}
@@ -142,6 +160,9 @@
name << "-" << data.config_str;
}
name << "_" << data.name;
+ if (!data.flag_name.empty()) {
+ name << ".(" << data.flag_name << ")";
+ }
if (!data.extension.empty()) {
name << "." << data.extension;
}
@@ -163,7 +184,6 @@
<< "failed to open file: " << fin->GetError());
return false;
}
-
// Parse the values file from XML.
xml::XmlPullParser xml_parser(fin.get());
@@ -176,6 +196,18 @@
// If visibility was forced, we need to use it when creating a new resource and also error if
// we try to parse the <public>, <public-group>, <java-symbol> or <symbol> tags.
parser_options.visibility = options.visibility;
+ parser_options.flag = ParseFlag(path_data.flag_name);
+
+ if (parser_options.flag) {
+ std::string error;
+ auto flag_status = GetFlagStatus(parser_options.flag, options.feature_flag_values, &error);
+ if (flag_status) {
+ parser_options.flag_status = std::move(flag_status.value());
+ } else {
+ context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error);
+ return false;
+ }
+ }
ResourceParser res_parser(context->GetDiagnostics(), &table, path_data.source, path_data.config,
parser_options);
@@ -402,6 +434,18 @@
xmlres->file.config = path_data.config;
xmlres->file.source = path_data.source;
xmlres->file.type = ResourceFile::Type::kProtoXml;
+ xmlres->file.flag = ParseFlag(path_data.flag_name);
+
+ if (xmlres->file.flag) {
+ std::string error;
+ auto flag_status = GetFlagStatus(xmlres->file.flag, options.feature_flag_values, &error);
+ if (flag_status) {
+ xmlres->file.flag_status = flag_status.value();
+ } else {
+ context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error);
+ return false;
+ }
+ }
// Collect IDs that are defined here.
XmlIdCollector collector;
@@ -491,6 +535,27 @@
res_file.source = path_data.source;
res_file.type = ResourceFile::Type::kPng;
+ if (!path_data.flag_name.empty()) {
+ FeatureFlagAttribute flag;
+ auto name = path_data.flag_name;
+ if (name.starts_with('!')) {
+ flag.negated = true;
+ flag.name = name.substr(1);
+ } else {
+ flag.name = name;
+ }
+ res_file.flag = flag;
+
+ std::string error;
+ auto flag_status = GetFlagStatus(flag, options.feature_flag_values, &error);
+ if (flag_status) {
+ res_file.flag_status = flag_status.value();
+ } else {
+ context->GetDiagnostics()->Error(android::DiagMessage(path_data.source) << error);
+ return false;
+ }
+ }
+
{
auto data = file->OpenAsData();
if (!data) {
diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp
index 6da3176..d3750a6 100644
--- a/tools/aapt2/cmd/Diff.cpp
+++ b/tools/aapt2/cmd/Diff.cpp
@@ -138,6 +138,22 @@
}
}
+ for (const ResourceConfigValue* config_value_a : entry_a.flag_disabled_values) {
+ auto config_value_b = entry_b.FindFlagDisabledValue(config_value_a->value->GetFlag().value(),
+ config_value_a->config);
+ if (!config_value_b) {
+ std::stringstream str_stream;
+ str_stream << "missing disabled value " << pkg_a.name << ":" << type_a.named_type << "/"
+ << entry_a.name << " config=" << config_value_a->config
+ << " flag=" << config_value_a->value->GetFlag()->ToString();
+ EmitDiffLine(apk_b->GetSource(), str_stream.str());
+ diff = true;
+ } else {
+ diff |= EmitResourceConfigValueDiff(context, apk_a, pkg_a, type_a, entry_a, config_value_a,
+ apk_b, pkg_b, type_b, entry_b, config_value_b);
+ }
+ }
+
// Check for any newly added config values.
for (const ResourceConfigValue* config_value_b : entry_b.values) {
auto config_value_a = entry_a.FindValue(config_value_b->config);
@@ -149,6 +165,18 @@
diff = true;
}
}
+ for (const ResourceConfigValue* config_value_b : entry_b.flag_disabled_values) {
+ auto config_value_a = entry_a.FindFlagDisabledValue(config_value_b->value->GetFlag().value(),
+ config_value_b->config);
+ if (!config_value_a) {
+ std::stringstream str_stream;
+ str_stream << "new disabled config " << pkg_b.name << ":" << type_b.named_type << "/"
+ << entry_b.name << " config=" << config_value_b->config
+ << " flag=" << config_value_b->value->GetFlag()->ToString();
+ EmitDiffLine(apk_b->GetSource(), str_stream.str());
+ diff = true;
+ }
+ }
return diff;
}
diff --git a/tools/aapt2/cmd/Util.cpp b/tools/aapt2/cmd/Util.cpp
index 7739171..08f8f0d 100644
--- a/tools/aapt2/cmd/Util.cpp
+++ b/tools/aapt2/cmd/Util.cpp
@@ -34,6 +34,44 @@
namespace aapt {
+std::optional<FeatureFlagAttribute> ParseFlag(std::optional<std::string_view> flag_text) {
+ if (!flag_text || flag_text->empty()) {
+ return {};
+ }
+ FeatureFlagAttribute flag;
+ if (flag_text->starts_with('!')) {
+ flag.negated = true;
+ flag.name = flag_text->substr(1);
+ } else {
+ flag.name = flag_text.value();
+ }
+ return flag;
+}
+
+std::optional<FlagStatus> GetFlagStatus(const std::optional<FeatureFlagAttribute>& flag,
+ const FeatureFlagValues& feature_flag_values,
+ std::string* out_err) {
+ if (!flag) {
+ return FlagStatus::NoFlag;
+ }
+ auto flag_it = feature_flag_values.find(flag->name);
+ if (flag_it == feature_flag_values.end()) {
+ *out_err = "Resource flag value undefined: " + flag->name;
+ return {};
+ }
+ const auto& flag_properties = flag_it->second;
+ if (!flag_properties.read_only) {
+ *out_err = "Only read only flags may be used with resources: " + flag->name;
+ return {};
+ }
+ if (!flag_properties.enabled.has_value()) {
+ *out_err = "Only flags with a value may be used with resources: " + flag->name;
+ return {};
+ }
+ return (flag_properties.enabled.value() != flag->negated) ? FlagStatus::Enabled
+ : FlagStatus::Disabled;
+}
+
std::optional<uint16_t> ParseTargetDensityParameter(StringPiece arg, android::IDiagnostics* diag) {
ConfigDescription preferred_density_config;
if (!ConfigDescription::Parse(arg, &preferred_density_config)) {
diff --git a/tools/aapt2/cmd/Util.h b/tools/aapt2/cmd/Util.h
index 6b8813b..d32e532 100644
--- a/tools/aapt2/cmd/Util.h
+++ b/tools/aapt2/cmd/Util.h
@@ -49,6 +49,12 @@
using FeatureFlagValues = std::map<std::string, FeatureFlagProperties, std::less<>>;
+std::optional<FeatureFlagAttribute> ParseFlag(std::optional<std::string_view> flag_text);
+
+std::optional<FlagStatus> GetFlagStatus(const std::optional<FeatureFlagAttribute>& flag,
+ const FeatureFlagValues& feature_flag_values,
+ std::string* out_err);
+
// Parses a configuration density (ex. hdpi, xxhdpi, 234dpi, anydpi, etc).
// Returns Nothing and logs a human friendly error message if the string was not legal.
std::optional<uint16_t> ParseTargetDensityParameter(android::StringPiece arg,
diff --git a/tools/aapt2/format/proto/ProtoDeserialize.cpp b/tools/aapt2/format/proto/ProtoDeserialize.cpp
index 55f5e56..8583cad 100644
--- a/tools/aapt2/format/proto/ProtoDeserialize.cpp
+++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp
@@ -536,6 +536,34 @@
config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config,
&out_table->string_pool, files, out_error);
+
+ if (config_value->value == nullptr) {
+ return false;
+ }
+ }
+
+ // flag disabled
+ for (const pb::ConfigValue& pb_config_value : pb_entry.flag_disabled_config_value()) {
+ const pb::Configuration& pb_config = pb_config_value.config();
+
+ ConfigDescription config;
+ if (!DeserializeConfigFromPb(pb_config, &config, out_error)) {
+ return false;
+ }
+
+ FeatureFlagAttribute flag;
+ flag.name = pb_config_value.value().item().flag_name();
+ flag.negated = pb_config_value.value().item().flag_negated();
+ ResourceConfigValue* config_value =
+ entry->FindOrCreateFlagDisabledValue(std::move(flag), config, pb_config.product());
+ if (config_value->value != nullptr) {
+ *out_error = "duplicate configuration in resource table";
+ return false;
+ }
+
+ config_value->value = DeserializeValueFromPb(pb_config_value.value(), src_pool, config,
+ &out_table->string_pool, files, out_error);
+
if (config_value->value == nullptr) {
return false;
}
@@ -615,6 +643,12 @@
out_file->source.path = pb_file.source_path();
out_file->type = DeserializeFileReferenceTypeFromPb(pb_file.type());
+ out_file->flag_status = (FlagStatus)pb_file.flag_status();
+ if (!pb_file.flag_name().empty()) {
+ out_file->flag =
+ FeatureFlagAttribute{.name = pb_file.flag_name(), .negated = pb_file.flag_negated()};
+ }
+
std::string config_error;
if (!DeserializeConfigFromPb(pb_file.config(), &out_file->config, &config_error)) {
std::ostringstream error;
@@ -748,7 +782,6 @@
if (value == nullptr) {
return {};
}
-
} else if (pb_value.has_compound_value()) {
const pb::CompoundValue& pb_compound_value = pb_value.compound_value();
switch (pb_compound_value.value_case()) {
@@ -1018,6 +1051,12 @@
DeserializeItemFromPbInternal(pb_item, src_pool, config, value_pool, files, out_error);
if (item) {
item->SetFlagStatus((FlagStatus)pb_item.flag_status());
+ if (!pb_item.flag_name().empty()) {
+ FeatureFlagAttribute flag;
+ flag.name = pb_item.flag_name();
+ flag.negated = pb_item.flag_negated();
+ item->SetFlag(std::move(flag));
+ }
}
return item;
}
diff --git a/tools/aapt2/format/proto/ProtoSerialize.cpp b/tools/aapt2/format/proto/ProtoSerialize.cpp
index 5772b3b..d83fe91 100644
--- a/tools/aapt2/format/proto/ProtoSerialize.cpp
+++ b/tools/aapt2/format/proto/ProtoSerialize.cpp
@@ -427,6 +427,14 @@
SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(),
source_pool.get());
}
+
+ for (const ResourceConfigValue* config_value : entry.flag_disabled_values) {
+ pb::ConfigValue* pb_config_value = pb_entry->add_flag_disabled_config_value();
+ SerializeConfig(config_value->config, pb_config_value->mutable_config());
+ pb_config_value->mutable_config()->set_product(config_value->product);
+ SerializeValueToPb(*config_value->value, pb_config_value->mutable_value(),
+ source_pool.get());
+ }
}
}
}
@@ -721,6 +729,11 @@
}
if (out_value->has_item()) {
out_value->mutable_item()->set_flag_status((uint32_t)value.GetFlagStatus());
+ if (value.GetFlag()) {
+ const auto& flag = value.GetFlag();
+ out_value->mutable_item()->set_flag_negated(flag->negated);
+ out_value->mutable_item()->set_flag_name(flag->name);
+ }
}
}
@@ -730,6 +743,11 @@
item.Accept(&serializer);
out_item->MergeFrom(value.item());
out_item->set_flag_status((uint32_t)item.GetFlagStatus());
+ if (item.GetFlag()) {
+ const auto& flag = item.GetFlag();
+ out_item->set_flag_negated(flag->negated);
+ out_item->set_flag_name(flag->name);
+ }
}
void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledFile* out_file) {
@@ -737,6 +755,11 @@
out_file->set_source_path(file.source.path);
out_file->set_type(SerializeFileReferenceTypeToPb(file.type));
SerializeConfig(file.config, out_file->mutable_config());
+ out_file->set_flag_status((uint32_t)file.flag_status);
+ if (file.flag) {
+ out_file->set_flag_negated(file.flag->negated);
+ out_file->set_flag_name(file.flag->name);
+ }
for (const SourcedResourceName& exported : file.exported_symbols) {
pb::internal::CompiledFile_Symbol* pb_symbol = out_file->add_exported_symbol();
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
index c456e5c..7160b35 100644
--- a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
@@ -31,13 +31,29 @@
"res/values/ints.xml",
"res/values/strings.xml",
"res/layout/layout1.xml",
+ "res/layout/layout3.xml",
+ "res/flag(test.package.falseFlag)/values/bools.xml",
+ "res/flag(test.package.falseFlag)/layout/layout2.xml",
+ "res/flag(test.package.falseFlag)/drawable/removedpng.png",
+ "res/flag(test.package.trueFlag)/layout/layout3.xml",
+ "res/values/flag(test.package.trueFlag)/bools.xml",
+ "res/values/flag(!test.package.trueFlag)/bools.xml",
+ "res/values/flag(!test.package.falseFlag)/bools.xml",
],
out: [
+ "drawable_removedpng.(test.package.falseFlag).png.flat",
"values_bools.arsc.flat",
+ "values_bools.(test.package.falseFlag).arsc.flat",
+ "values_bools.(test.package.trueFlag).arsc.flat",
+ "values_bools.(!test.package.falseFlag).arsc.flat",
+ "values_bools.(!test.package.trueFlag).arsc.flat",
"values_bools2.arsc.flat",
"values_ints.arsc.flat",
"values_strings.arsc.flat",
"layout_layout1.xml.flat",
+ "layout_layout2.(test.package.falseFlag).xml.flat",
+ "layout_layout3.xml.flat",
+ "layout_layout3.(test.package.trueFlag).xml.flat",
],
cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
"--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/drawable/removedpng.png" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/drawable/removedpng.png"
new file mode 100644
index 0000000..8a9e698
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/drawable/removedpng.png"
Binary files differ
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/layout/layout2.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/layout/layout2.xml"
new file mode 100644
index 0000000..dec5de7
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/layout/layout2.xml"
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+</LinearLayout>
\ No newline at end of file
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/values/bools.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/values/bools.xml"
new file mode 100644
index 0000000..c46c4d4
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.falseFlag\051/values/bools.xml"
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="bool7">false</bool>
+</resources>
\ No newline at end of file
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.trueFlag\051/layout/layout3.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.trueFlag\051/layout/layout3.xml"
new file mode 100644
index 0000000..5aeee0e
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag\050test.package.trueFlag\051/layout/layout3.xml"
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+ <TextView android:id="@+id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="foobar" />
+</LinearLayout>
\ No newline at end of file
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml
new file mode 100644
index 0000000..dec5de7
--- /dev/null
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout3.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+</LinearLayout>
\ No newline at end of file
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
index 1ed0c8a..35975ed 100644
--- a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
@@ -9,4 +9,15 @@
<bool name="bool3">false</bool>
<bool name="bool4" android:featureFlag="test.package.falseFlag">true</bool>
+
+ <bool name="bool5">false</bool>
+ <bool name="bool5" android:featureFlag="!test.package.falseFlag">true</bool>
+
+ <bool name="bool6">true</bool>
+ <bool name="bool6" android:featureFlag="!test.package.trueFlag">false</bool>
+
+ <bool name="bool7">true</bool>
+ <bool name="bool8">false</bool>
+ <bool name="bool9">true</bool>
+ <bool name="bool10">false</bool>
</resources>
\ No newline at end of file
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.falseFlag\051/bools.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.falseFlag\051/bools.xml"
new file mode 100644
index 0000000..a63749c
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.falseFlag\051/bools.xml"
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="bool10">true</bool>
+</resources>
\ No newline at end of file
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.trueFlag\051/bools.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.trueFlag\051/bools.xml"
new file mode 100644
index 0000000..bb5526e
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050\041test.package.trueFlag\051/bools.xml"
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="bool9">false</bool>
+</resources>
\ No newline at end of file
diff --git "a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050test.package.trueFlag\051/bools.xml" "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050test.package.trueFlag\051/bools.xml"
new file mode 100644
index 0000000..eba780e
--- /dev/null
+++ "b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag\050test.package.trueFlag\051/bools.xml"
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="bool8">true</bool>
+</resources>
\ No newline at end of file
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index 3db37c2..6293008 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -17,6 +17,7 @@
#include "LoadedApk.h"
#include "cmd/Dump.h"
#include "io/StringStream.h"
+#include "test/Common.h"
#include "test/Test.h"
#include "text/Printer.h"
@@ -75,6 +76,10 @@
std::string output;
DumpResourceTableToString(loaded_apk.get(), &output);
+ ASSERT_EQ(output.find("bool4"), std::string::npos);
+ ASSERT_EQ(output.find("str1"), std::string::npos);
+ ASSERT_EQ(output.find("layout2"), std::string::npos);
+ ASSERT_EQ(output.find("removedpng"), std::string::npos);
}
TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTableChunks) {
@@ -86,6 +91,8 @@
ASSERT_EQ(output.find("bool4"), std::string::npos);
ASSERT_EQ(output.find("str1"), std::string::npos);
+ ASSERT_EQ(output.find("layout2"), std::string::npos);
+ ASSERT_EQ(output.find("removedpng"), std::string::npos);
}
TEST_F(FlaggedResourcesTest, DisabledResourcesInRJava) {
@@ -98,4 +105,47 @@
ASSERT_NE(r_contents.find("public static final int str1"), std::string::npos);
}
+TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlag) {
+ test::TestDiagnosticsImpl diag;
+ const std::string compiled_files_dir = GetTestPath("compiled");
+ ASSERT_FALSE(CompileFile(
+ GetTestPath("res/values/values.xml"),
+ R"(<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool>
+ <bool name="bool1" android:featureFlag="test.package.falseFlag">true</bool>
+ </resources>)",
+ compiled_files_dir, &diag,
+ {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"}));
+ ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool/bool1'"));
+}
+
+TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlagDifferentFiles) {
+ test::TestDiagnosticsImpl diag;
+ const std::string compiled_files_dir = GetTestPath("compiled");
+ ASSERT_TRUE(CompileFile(
+ GetTestPath("res/values/values1.xml"),
+ R"(<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <bool name="bool1" android:featureFlag="test.package.falseFlag">false</bool>
+ </resources>)",
+ compiled_files_dir, &diag,
+ {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"}));
+ ASSERT_TRUE(CompileFile(
+ GetTestPath("res/values/values2.xml"),
+ R"(<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <bool name="bool1" android:featureFlag="test.package.falseFlag">true</bool>
+ </resources>)",
+ compiled_files_dir, &diag,
+ {"--feature-flags", "test.package.falseFlag:ro=false,test.package.trueFlag:ro=true"}));
+ const std::string out_apk = GetTestPath("out.apk");
+ std::vector<std::string> link_args = {
+ "--manifest",
+ GetDefaultManifest(),
+ "-o",
+ out_apk,
+ };
+
+ ASSERT_FALSE(Link(link_args, compiled_files_dir, &diag));
+ ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool1'"));
+}
+
} // namespace aapt
diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp
index 37a039e..1bef5f8 100644
--- a/tools/aapt2/link/TableMerger.cpp
+++ b/tools/aapt2/link/TableMerger.cpp
@@ -321,6 +321,30 @@
}
}
}
+
+ // disabled values
+ for (auto& src_config_value : src_entry->flag_disabled_values) {
+ auto dst_config_value = dst_entry->FindOrCreateFlagDisabledValue(
+ src_config_value->value->GetFlag().value(), src_config_value->config,
+ src_config_value->product);
+ if (!dst_config_value->value) {
+ // Resource does not exist, add it now.
+ // Must clone the value since it might be in the values vector as well
+ CloningValueTransformer cloner(&main_table_->string_pool);
+ dst_config_value->value = src_config_value->value->Transform(cloner);
+ } else {
+ error = true;
+ context_->GetDiagnostics()->Error(
+ android::DiagMessage(src_config_value->value->GetSource())
+ << "duplicate value for resource '" << src_entry->name << "' " << "with config '"
+ << src_config_value->config << "' and flag '"
+ << (src_config_value->value->GetFlag()->negated ? "!" : "")
+ << src_config_value->value->GetFlag()->name << "'");
+ context_->GetDiagnostics()->Note(
+ android::DiagMessage(dst_config_value->value->GetSource())
+ << "resource previously defined here");
+ }
+ }
}
}
return !error;
@@ -353,6 +377,8 @@
file_ref->SetSource(file_desc.source);
file_ref->type = file_desc.type;
file_ref->file = file;
+ file_ref->SetFlagStatus(file_desc.flag_status);
+ file_ref->SetFlag(file_desc.flag);
ResourceTablePackage* pkg = table.FindOrCreatePackage(file_desc.name.package);
pkg->FindOrCreateType(file_desc.name.type)
diff --git a/tools/aapt2/test/Common.cpp b/tools/aapt2/test/Common.cpp
index cdf24534..c7dd4c9 100644
--- a/tools/aapt2/test/Common.cpp
+++ b/tools/aapt2/test/Common.cpp
@@ -21,23 +21,6 @@
namespace aapt {
namespace test {
-struct TestDiagnosticsImpl : public android::IDiagnostics {
- void Log(Level level, android::DiagMessageActual& actual_msg) override {
- switch (level) {
- case Level::Note:
- return;
-
- case Level::Warn:
- std::cerr << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl;
- break;
-
- case Level::Error:
- std::cerr << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl;
- break;
- }
- }
-};
-
android::IDiagnostics* GetDiagnostics() {
static TestDiagnosticsImpl diag;
return &diag;
diff --git a/tools/aapt2/test/Common.h b/tools/aapt2/test/Common.h
index 0437980..b06c432 100644
--- a/tools/aapt2/test/Common.h
+++ b/tools/aapt2/test/Common.h
@@ -37,6 +37,32 @@
namespace aapt {
namespace test {
+struct TestDiagnosticsImpl : public android::IDiagnostics {
+ void Log(Level level, android::DiagMessageActual& actual_msg) override {
+ switch (level) {
+ case Level::Note:
+ return;
+
+ case Level::Warn:
+ std::cerr << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl;
+ log << actual_msg.source << ": warn: " << actual_msg.message << "." << std::endl;
+ break;
+
+ case Level::Error:
+ std::cerr << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl;
+ log << actual_msg.source << ": error: " << actual_msg.message << "." << std::endl;
+ break;
+ }
+ }
+
+ std::string GetLog() {
+ return log.str();
+ }
+
+ private:
+ std::ostringstream log;
+};
+
android::IDiagnostics* GetDiagnostics();
inline ResourceName ParseNameOrDie(android::StringPiece str) {
diff --git a/tools/aapt2/test/Fixture.cpp b/tools/aapt2/test/Fixture.cpp
index b91abe5..570bcf1 100644
--- a/tools/aapt2/test/Fixture.cpp
+++ b/tools/aapt2/test/Fixture.cpp
@@ -91,10 +91,13 @@
}
bool CommandTestFixture::CompileFile(const std::string& path, const std::string& contents,
- android::StringPiece out_dir, android::IDiagnostics* diag) {
+ android::StringPiece out_dir, android::IDiagnostics* diag,
+ const std::vector<android::StringPiece>& additional_args) {
WriteFile(path, contents);
CHECK(file::mkdirs(out_dir.data()));
- return CompileCommand(diag).Execute({path, "-o", out_dir, "-v"}, &std::cerr) == 0;
+ std::vector<android::StringPiece> args = {path, "-o", out_dir, "-v"};
+ args.insert(args.end(), additional_args.begin(), additional_args.end());
+ return CompileCommand(diag).Execute(args, &std::cerr) == 0;
}
bool CommandTestFixture::Link(const std::vector<std::string>& args, android::IDiagnostics* diag) {
diff --git a/tools/aapt2/test/Fixture.h b/tools/aapt2/test/Fixture.h
index 14298d16..178d011 100644
--- a/tools/aapt2/test/Fixture.h
+++ b/tools/aapt2/test/Fixture.h
@@ -73,7 +73,8 @@
// Wries the contents of the file to the specified path. The file is compiled and the flattened
// file is written to the out directory.
bool CompileFile(const std::string& path, const std::string& contents,
- android::StringPiece flat_out_dir, android::IDiagnostics* diag);
+ android::StringPiece flat_out_dir, android::IDiagnostics* diag,
+ const std::vector<android::StringPiece>& additional_args = {});
// Executes the link command with the specified arguments.
bool Link(const std::vector<std::string>& args, android::IDiagnostics* diag);