Merge "Add null check to motion events in global actions dialog" into main
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..bfddf4f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6822,6 +6822,17 @@
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 class SoundTrigger.RecognitionEvent {
method @Nullable public android.media.AudioFormat getCaptureFormat();
method public int getCaptureSession();
@@ -7772,10 +7783,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 +13037,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..cc9e836 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);
@@ -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 {
+ @FlaggedApi("android.media.soundtrigger.manager_api") 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);
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/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/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..79cbd19 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,56 @@
* 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)}
+ *
+ * @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
+ */
+ @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 +1581,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 +1637,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 +1651,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 +1666,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,11 +1688,11 @@
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;
}
}
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/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/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/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/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/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/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..fc63c0f
--- /dev/null
+++ b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml
@@ -0,0 +1,104 @@
+<?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" />
+ <TextView
+ android:id="@+id/text1"
+ style="@style/SettingsLibActionButton.Expressive.Label"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+
+ </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" />
+ <TextView
+ android:id="@+id/text2"
+ style="@style/SettingsLibActionButton.Expressive.Label"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+
+ </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" />
+ <TextView
+ android:id="@+id/text3"
+ style="@style/SettingsLibActionButton.Expressive.Label"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+
+ </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" />
+ <TextView
+ android:id="@+id/text4"
+ style="@style/SettingsLibActionButton.Expressive.Label"
+ android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3"/>
+ </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..f011039 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,43 @@
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);
+ if (mIsExpressive) {
+ mTextView.setText(mText);
+ if (mButton instanceof MaterialButton) {
+ ((MaterialButton) mButton).setIcon(mIcon);
+ }
+ } else {
+ mButton.setText(mText);
+ mButton.setCompoundDrawablesWithIntrinsicBounds(
+ null /* left */, mIcon /* top */, null /* right */, null /* bottom */);
+ }
+
mButton.setOnClickListener(mListener);
mButton.setEnabled(mIsEnabled);
- mButton.setCompoundDrawablesWithIntrinsicBounds(
- null /* left */, mIcon /* top */, null /* right */, null /* bottom */);
+
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..d4851e1 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -32,6 +32,7 @@
"SettingsLibBannerMessagePreference",
"SettingsLibBarChartPreference",
"SettingsLibButtonPreference",
+ "SettingsLibBulletPreference",
"SettingsLibCollapsingToolbarBaseActivity",
"SettingsLibDeviceStateRotationLock",
"SettingsLibDisplayUtils",
@@ -53,6 +54,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/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..816433c 100644
--- a/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles_expressive.xml
@@ -169,4 +169,28 @@
<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>
</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/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/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/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/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/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/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..820c102 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -35,6 +35,8 @@
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
@@ -56,6 +58,8 @@
NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
NotificationMinimalismPrototype.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/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/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/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/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/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/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/emptyshade/shared/ModesEmptyShadeFix.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
new file mode 100644
index 0000000..f1fc275
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/emptyshade/shared/ModesEmptyShadeFix.kt
@@ -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.systemui.statusbar.notification.emptyshade.shared
+
+import android.app.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the modes_ui_empty_shade flag state. */
+@Suppress("NOTHING_TO_INLINE")
+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 refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.modesUiEmptyShade()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * 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
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
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/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/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/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/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/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/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/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..15277ce 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());
@@ -1100,14 +1102,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 +3021,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 +3091,7 @@
}
private void dumpUsageStats(FileDescriptor fd, PrintWriter pw, int model,
- boolean proto) {
+ boolean proto, boolean accumulated) {
awaitCompletion();
syncStats("dump", BatteryExternalStatsWorker.UPDATE_ALL);
@@ -3097,6 +3105,9 @@
if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
builder.powerProfileModeledOnly();
}
+ if (accumulated) {
+ builder.accumulated();
+ }
BatteryUsageStatsQuery query = builder.build();
synchronized (mStats) {
mStats.prepareForDumpLocked();
@@ -3287,6 +3298,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 +3321,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/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/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/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/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/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/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java
index 090e679..5b22c10 100644
--- a/services/core/java/com/android/server/vibrator/VibrationThread.java
+++ b/services/core/java/com/android/server/vibrator/VibrationThread.java
@@ -16,6 +16,8 @@
package com.android.server.vibrator;
+import static android.os.Trace.TRACE_TAG_VIBRATOR;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.PowerManager;
@@ -117,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) {
@@ -129,7 +131,7 @@
}
return true;
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -249,17 +251,17 @@
private void clientVibrationCompleteIfNotAlready(@NonNull Vibration.EndInfo vibrationEndInfo) {
if (!mCalledVibrationCompleteCallback) {
mCalledVibrationCompleteCallback = true;
- Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "notifyVibrationComplete");
+ Trace.traceBegin(TRACE_TAG_VIBRATOR, "notifyVibrationComplete");
try {
mExecutingConductor.notifyVibrationComplete(vibrationEndInfo);
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ 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()) {
@@ -283,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 bd17e5d..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,
@@ -671,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,
@@ -708,7 +704,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -899,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);
@@ -919,7 +915,7 @@
mNextVibration = conductor;
return null;
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -930,7 +926,7 @@
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()) {
@@ -1581,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
@@ -1590,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:
@@ -1635,20 +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);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -1657,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) {
@@ -1686,7 +1683,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
}
@@ -1994,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.
@@ -2116,13 +2113,13 @@
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
@@ -2135,7 +2132,7 @@
}
}
} finally {
- Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR);
+ Trace.traceEnd(TRACE_TAG_VIBRATOR);
}
}
@@ -2203,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/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/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/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/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/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/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index bbe0755..ac1b7c6 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -54,6 +54,7 @@
"services.flags",
"services.net",
"services.people",
+ "services.supervision",
"services.usage",
"service-permission.stubs.system_server",
"guava",
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/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/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/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/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);