diff options
352 files changed, 11444 insertions, 3321 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 0f3b1c366fb0..033da2df9bf6 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -4944,10 +4944,14 @@ public class AlarmManagerService extends SystemService { @Override public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action == null) { + return; + } final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); synchronized (mLock) { String pkgList[] = null; - switch (intent.getAction()) { + switch (action) { case Intent.ACTION_QUERY_PACKAGE_RESTART: pkgList = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); for (String packageName : pkgList) { diff --git a/core/api/current.txt b/core/api/current.txt index 2c6e1cb44c7f..8eb881139b34 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -7015,6 +7015,7 @@ package android.app { 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 cc0354c32de2..a1561c242027 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -6822,6 +6822,27 @@ package android.hardware.soundtrigger { field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.ModuleProperties> CREATOR; } + @FlaggedApi("android.media.soundtrigger.manager_api") public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable { + method public int describeContents(); + method public int getAudioCapabilities(); + method @NonNull public byte[] getData(); + method @NonNull public java.util.List<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra> getKeyphrases(); + method public boolean isAllowMultipleTriggers(); + method public boolean isCaptureRequested(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR; + } + + public static final class SoundTrigger.RecognitionConfig.Builder { + ctor public SoundTrigger.RecognitionConfig.Builder(); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig build(); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setAllowMultipleTriggers(boolean); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setAudioCapabilities(int); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setCaptureRequested(boolean); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setData(@Nullable byte[]); + method @NonNull public android.hardware.soundtrigger.SoundTrigger.RecognitionConfig.Builder setKeyphrases(@NonNull java.util.Collection<android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra>); + } + public static class SoundTrigger.RecognitionEvent { method @Nullable public android.media.AudioFormat getCaptureFormat(); method public int getCaptureSession(); @@ -7772,10 +7793,16 @@ package android.media.soundtrigger { method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void deleteModel(java.util.UUID); method public int getDetectionServiceOperationsTimeout(); method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.media.soundtrigger.SoundTriggerManager.Model getModel(java.util.UUID); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getModelState(@NonNull java.util.UUID); method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModuleProperties getModuleProperties(); method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int getParameter(@NonNull java.util.UUID, int); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public boolean isRecognitionActive(@NonNull java.util.UUID); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int loadSoundModel(@NonNull android.hardware.soundtrigger.SoundTrigger.SoundModel); method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public android.hardware.soundtrigger.SoundTrigger.ModelParamRange queryParameter(@Nullable java.util.UUID, int); method @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int setParameter(@Nullable java.util.UUID, int, int); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int startRecognition(@NonNull java.util.UUID, @Nullable android.os.Bundle, @NonNull android.content.ComponentName, @NonNull android.hardware.soundtrigger.SoundTrigger.RecognitionConfig); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int stopRecognition(@NonNull java.util.UUID); + method @FlaggedApi("android.media.soundtrigger.manager_api") @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public int unloadSoundModel(@NonNull java.util.UUID); method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) public void updateModel(android.media.soundtrigger.SoundTriggerManager.Model); } @@ -13020,6 +13047,8 @@ package android.service.notification { 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 72a68f85b6b7..6511c214ee52 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -398,6 +398,7 @@ package android.app { 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 @@ package android.hardware.soundtrigger { ctor public SoundTrigger.ModuleProperties(int, @NonNull String, @NonNull String, @NonNull String, int, @NonNull String, int, int, int, int, boolean, int, boolean, int, boolean, int); } - public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable { - ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int); + @FlaggedApi("android.media.soundtrigger.manager_api") public static final class SoundTrigger.RecognitionConfig implements android.os.Parcelable { + ctor @Deprecated public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[], int); ctor public SoundTrigger.RecognitionConfig(boolean, boolean, @Nullable android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[], @Nullable byte[]); - method public int describeContents(); - method public void writeToParcel(@NonNull android.os.Parcel, int); - field @NonNull public static final android.os.Parcelable.Creator<android.hardware.soundtrigger.SoundTrigger.RecognitionConfig> CREATOR; - field public final boolean allowMultipleTriggers; - field public final int audioCapabilities; - field public final boolean captureRequested; - field @NonNull public final byte[] data; - field @NonNull public final android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra[] keyphrases; } public static class SoundTrigger.RecognitionEvent { @@ -2216,7 +2209,7 @@ package android.media.soundtrigger { 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 @@ package android.media.soundtrigger { 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.bp b/core/java/Android.bp index 92bca3cfbef2..99046328b1e2 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -21,14 +21,52 @@ filegroup { "**/*.aidl", ":framework-nfc-non-updatable-sources", ":messagequeue-gen", + ":ranging_stack_mock_initializer", ], // Exactly one MessageQueue.java will be added to srcs by messagequeue-gen exclude_srcs: [ "android/os/*MessageQueue/**/*.java", + "android/ranging/**/*.java", ], visibility: ["//frameworks/base"], } +//Mock to allow service registry for ranging stack. +//TODO(b/331206299): Remove this after RELEASE_RANGING_STACK is ramped up to next. +soong_config_module_type { + name: "ranging_stack_framework_mock_init", + module_type: "genrule", + config_namespace: "bootclasspath", + bool_variables: [ + "release_ranging_stack", + ], + properties: [ + "srcs", + "cmd", + "out", + ], +} + +// The actual RangingFrameworkInitializer is present in packages/modules/Uwb/ranging/framework. +// Mock RangingFrameworkInitializer does nothing and allows to successfully build +// SystemServiceRegistry after registering for system service in SystemServiceRegistry both with +// and without build flag RELEASE_RANGING_STACK enabled. +ranging_stack_framework_mock_init { + name: "ranging_stack_mock_initializer", + soong_config_variables: { + release_ranging_stack: { + cmd: "touch $(out)", + // Adding an empty file as out is mandatory. + out: ["android/ranging/empty_ranging_fw.txt"], + conditions_default: { + srcs: ["android/ranging/mock/RangingFrameworkInitializer.java"], + cmd: "mkdir -p android/ranging/; cp $(in) $(out);", + out: ["android/ranging/RangingFrameworkInitializer.java"], + }, + }, + }, +} + // Add selected MessageQueue.java implementation to srcs soong_config_module_type { name: "release_package_messagequeue_implementation_srcs", diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java index fad289c1beb8..edcdb6cc58ea 100644 --- a/core/java/android/app/ApplicationStartInfo.java +++ b/core/java/android/app/ApplicationStartInfo.java @@ -1047,7 +1047,7 @@ public final class ApplicationStartInfo implements Parcelable { 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 b9fe356690e0..8a54b5d4ec4f 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -258,6 +258,7 @@ interface INotificationManager @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 83f9ff733ec8..c7b84ae6283b 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -953,6 +953,36 @@ public class NotificationManager { } /** + * 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 0e761fce9346..c17da249f322 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -54,198 +54,8 @@ import java.util.concurrent.atomic.AtomicLong; * 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/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index c13a58f52ac8..ea4148c8ffa1 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -230,6 +230,7 @@ import android.print.IPrintManager; import android.print.PrintManager; import android.provider.E2eeContactKeysManager; import android.provider.ProviderFrameworkInitializer; +import android.ranging.RangingFrameworkInitializer; import android.safetycenter.SafetyCenterFrameworkInitializer; import android.scheduling.SchedulingFrameworkInitializer; import android.security.FileIntegrityManager; @@ -1825,6 +1826,12 @@ public final class SystemServiceRegistry { if (android.webkit.Flags.updateServiceIpcWrapper()) { WebViewBootstrapFrameworkInitializer.registerServiceWrappers(); } + // This is guarded by aconfig flag "com.android.ranging.flags.ranging_stack_enabled" + // when the build flag RELEASE_RANGING_STACK is enabled. When disabled, this calls the + // mock RangingFrameworkInitializer#registerServiceWrappers which is no-op. As the + // aconfig lib for ranging module is built only if RELEASE_RANGING_STACK is enabled, + // flagcannot be added here. + RangingFrameworkInitializer.registerServiceWrappers(); } finally { // If any of the above code throws, we're in a pretty bad shape and the process // will likely crash, but we'll reset it just in case there's an exception handler... diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index daa15f05d942..9be928f7efd0 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -213,19 +213,17 @@ import java.util.function.Consumer; * <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 @@ import java.util.function.Consumer; * <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 @@ import java.util.function.Consumer; * 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 @@ Policy Management Role} and is trusted with managing the overall state of * * <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 @@ public class DevicePolicyManager { * @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 108b5f40863c..b13901721909 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -31,6 +31,16 @@ flag { } 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 8d25cad2fc67..4598421eb3bc 100644 --- a/core/java/android/app/supervision/ISupervisionManager.aidl +++ b/core/java/android/app/supervision/ISupervisionManager.aidl @@ -21,5 +21,5 @@ package android.app.supervision; * {@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 8611a92074c0..aee1cd9b4760 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 @@ public class SupervisionManager { * * @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 abb562d8ddaf..d8142fd9687c 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -1042,10 +1042,11 @@ public class AppWidgetManager { } /** - * 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 @@ public class AppWidgetManager { * * @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 139ff65b4d86..160cbdffe5bb 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -293,4 +293,19 @@ flag { 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 a2d24f643bfb..73b5d947c0fe 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -432,6 +432,11 @@ public abstract class DisplayManagerInternal { 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 bdbec5596ade..5ee61bcd436a 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -48,49 +48,57 @@ public final class KeyGestureEvent { 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 @@ public final class KeyGestureEvent { 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 @@ public final class KeyGestureEvent { 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 @@ public final class KeyGestureEvent { 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 @@ public final class KeyGestureEvent { 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 @@ public final class KeyGestureEvent { 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 5c07fa48d466..22ae67672950 100644 --- a/core/java/android/hardware/soundtrigger/ConversionUtil.java +++ b/core/java/android/hardware/soundtrigger/ConversionUtil.java @@ -155,16 +155,16 @@ public class ConversionUtil { 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 9f3e3ad8c01e..05e91e447a43 100644 --- a/core/java/android/hardware/soundtrigger/SoundTrigger.java +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -63,6 +63,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.UUID; @@ -1513,50 +1514,60 @@ public class SoundTrigger { * 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; + private final boolean mCaptureRequested; + private final boolean mAllowMultipleTriggers; + private final KeyphraseRecognitionExtra mKeyphrases[]; + private final byte[] mData; + @ModuleProperties.AudioCapabilities + private final int mAudioCapabilities; /** - * Bit field encoding of the AudioCapabilities - * supported by the firmware. + * Constructor for {@link RecognitionConfig} with {@code audioCapabilities} describes a + * config that can be used by + * {@link SoundTriggerModule#startRecognition(int, RecognitionConfig)} + * + * @deprecated should use builder-based constructor instead. + * TODO(b/368042125): remove this method. + * @param captureRequested Whether the DSP should capture the trigger sound. + * @param allowMultipleTriggers Whether the service should restart listening after the DSP + * triggers. + * @param keyphrases List of keyphrases in the sound model. + * @param data Opaque data for use by system applications who know about voice engine + * internals, typically during enrollment. + * @param audioCapabilities Bit field encoding of the AudioCapabilities. + * + * @hide */ - @ModuleProperties.AudioCapabilities - public final int audioCapabilities; - + @Deprecated + @SuppressWarnings("Todo") + @TestApi public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers, @SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases, @Nullable byte[] data, int audioCapabilities) { - this.captureRequested = captureRequested; - this.allowMultipleTriggers = allowMultipleTriggers; - this.keyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0]; - this.data = data != null ? data : new byte[0]; - this.audioCapabilities = audioCapabilities; + this.mCaptureRequested = captureRequested; + this.mAllowMultipleTriggers = allowMultipleTriggers; + this.mKeyphrases = keyphrases != null ? keyphrases : new KeyphraseRecognitionExtra[0]; + this.mData = data != null ? data : new byte[0]; + this.mAudioCapabilities = audioCapabilities; } + /** + * Constructor for {@link RecognitionConfig} without audioCapabilities. The + * audioCapabilities is set to 0. + * + * @param captureRequested Whether the DSP should capture the trigger sound. + * @param allowMultipleTriggers Whether the service should restart listening after the DSP + * triggers. + * @param keyphrases List of keyphrases in the sound model. + * @param data Opaque data for use by system applications. + * + * @hide + */ @UnsupportedAppUsage + @TestApi public RecognitionConfig(boolean captureRequested, boolean allowMultipleTriggers, @SuppressLint("ArrayReturn") @Nullable KeyphraseRecognitionExtra[] keyphrases, @Nullable byte[] data) { @@ -1574,9 +1585,52 @@ public class SoundTrigger { } }; + /** + * Returns whether the DSP should capture the trigger sound and make it available for + * further capture. + */ + public boolean isCaptureRequested() { + return mCaptureRequested; + } + + /** + * Returns whether the service should restart listening after the DSP triggers. + * + * <p><b>Note:</b> This config flag is currently used at the service layer rather than by + * the DSP. + */ + public boolean isAllowMultipleTriggers() { + return mAllowMultipleTriggers; + } + + /** + * Gets all keyphrases in the sound model for which recognition should be performed with + * options for each keyphrase. + */ + @NonNull + public List<KeyphraseRecognitionExtra> getKeyphrases() { + return Arrays.asList(mKeyphrases); + } + + /** + * Opaque data. + * + * <p>For use by system applications who knows about voice engine internals, typically + * during enrollment. + */ + @NonNull + public byte[] getData() { + return mData; + } + + /** Bit field encoding of the AudioCapabilities supported by the firmware. */ + public int getAudioCapabilities() { + return mAudioCapabilities; + } + private static RecognitionConfig fromParcel(Parcel in) { - boolean captureRequested = in.readByte() == 1; - boolean allowMultipleTriggers = in.readByte() == 1; + boolean captureRequested = in.readBoolean(); + boolean allowMultipleTriggers = in.readBoolean(); KeyphraseRecognitionExtra[] keyphrases = in.createTypedArray(KeyphraseRecognitionExtra.CREATOR); byte[] data = in.readBlob(); @@ -1587,11 +1641,11 @@ public class SoundTrigger { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeByte((byte) (captureRequested ? 1 : 0)); - dest.writeByte((byte) (allowMultipleTriggers ? 1 : 0)); - dest.writeTypedArray(keyphrases, flags); - dest.writeBlob(data); - dest.writeInt(audioCapabilities); + dest.writeBoolean(mCaptureRequested); + dest.writeBoolean(mAllowMultipleTriggers); + dest.writeTypedArray(mKeyphrases, flags); + dest.writeBlob(mData); + dest.writeInt(mAudioCapabilities); } @Override @@ -1601,10 +1655,10 @@ public class SoundTrigger { @Override public String toString() { - return "RecognitionConfig [captureRequested=" + captureRequested - + ", allowMultipleTriggers=" + allowMultipleTriggers + ", keyphrases=" - + Arrays.toString(keyphrases) + ", data=" + Arrays.toString(data) - + ", audioCapabilities=" + Integer.toHexString(audioCapabilities) + "]"; + return "RecognitionConfig [captureRequested=" + mCaptureRequested + + ", allowMultipleTriggers=" + mAllowMultipleTriggers + ", keyphrases=" + + Arrays.toString(mKeyphrases) + ", data=" + Arrays.toString(mData) + + ", audioCapabilities=" + Integer.toHexString(mAudioCapabilities) + "]"; } @Override @@ -1616,19 +1670,19 @@ public class SoundTrigger { if (!(obj instanceof RecognitionConfig)) return false; RecognitionConfig other = (RecognitionConfig) obj; - if (captureRequested != other.captureRequested) { + if (mCaptureRequested != other.mCaptureRequested) { return false; } - if (allowMultipleTriggers != other.allowMultipleTriggers) { + if (mAllowMultipleTriggers != other.mAllowMultipleTriggers) { return false; } - if (!Arrays.equals(keyphrases, other.keyphrases)) { + if (!Arrays.equals(mKeyphrases, other.mKeyphrases)) { return false; } - if (!Arrays.equals(data, other.data)) { + if (!Arrays.equals(mData, other.mData)) { return false; } - if (audioCapabilities != other.audioCapabilities) { + if (mAudioCapabilities != other.mAudioCapabilities) { return false; } return true; @@ -1638,13 +1692,96 @@ public class SoundTrigger { public final int hashCode() { final int prime = 31; int result = 1; - result = prime * result + (captureRequested ? 1 : 0); - result = prime * result + (allowMultipleTriggers ? 1 : 0); - result = prime * result + Arrays.hashCode(keyphrases); - result = prime * result + Arrays.hashCode(data); - result = prime * result + audioCapabilities; + result = prime * result + (mCaptureRequested ? 1 : 0); + result = prime * result + (mAllowMultipleTriggers ? 1 : 0); + result = prime * result + Arrays.hashCode(mKeyphrases); + result = prime * result + Arrays.hashCode(mData); + result = prime * result + mAudioCapabilities; return result; } + + /** + * Builder class for {@link RecognitionConfig} objects. + */ + public static final class Builder { + private boolean mCaptureRequested; + private boolean mAllowMultipleTriggers; + @Nullable private KeyphraseRecognitionExtra[] mKeyphrases; + @Nullable private byte[] mData; + private int mAudioCapabilities; + + /** + * Constructs a new Builder with the default values. + */ + public Builder() { + } + + /** + * Sets capture requested state. + * @param captureRequested The new requested state. + * @return the same Builder instance. + */ + public @NonNull Builder setCaptureRequested(boolean captureRequested) { + mCaptureRequested = captureRequested; + return this; + } + + /** + * Sets allow multiple triggers state. + * @param allowMultipleTriggers The new allow multiple triggers state. + * @return the same Builder instance. + */ + public @NonNull Builder setAllowMultipleTriggers(boolean allowMultipleTriggers) { + mAllowMultipleTriggers = allowMultipleTriggers; + return this; + } + + /** + * Sets the keyphrases field. + * @param keyphrases The new keyphrases. + * @return the same Builder instance. + */ + public @NonNull Builder setKeyphrases( + @NonNull Collection<KeyphraseRecognitionExtra> keyphrases) { + mKeyphrases = keyphrases.toArray(new KeyphraseRecognitionExtra[keyphrases.size()]); + return this; + } + + /** + * Sets the data field. + * @param data The new data. + * @return the same Builder instance. + */ + public @NonNull Builder setData(@Nullable byte[] data) { + mData = data; + return this; + } + + /** + * Sets the audio capabilities field. + * @param audioCapabilities The new audio capabilities. + * @return the same Builder instance. + */ + public @NonNull Builder setAudioCapabilities(int audioCapabilities) { + mAudioCapabilities = audioCapabilities; + return this; + } + + /** + * Combines all of the parameters that have been set and return a new + * {@link RecognitionConfig} object. + * @return a new {@link RecognitionConfig} object + */ + public @NonNull RecognitionConfig build() { + RecognitionConfig config = new RecognitionConfig( + /* captureRequested= */ mCaptureRequested, + /* allowMultipleTriggers= */ mAllowMultipleTriggers, + /* keyphrases= */ mKeyphrases, + /* data= */ mData, + /* audioCapabilities= */ mAudioCapabilities); + return config; + } + }; } /** diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java index 91cdf8d8fcae..1c9be6fb4b82 100644 --- a/core/java/android/net/vcn/VcnManager.java +++ b/core/java/android/net/vcn/VcnManager.java @@ -76,11 +76,15 @@ import java.util.concurrent.Executor; * 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 1fef602b8dd7..a698b9d97215 100644 --- a/core/java/android/os/BatteryUsageStats.java +++ b/core/java/android/os/BatteryUsageStats.java @@ -788,6 +788,12 @@ public final class BatteryUsageStats implements Parcelable, Closeable { /** 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 @@ public final class BatteryUsageStats implements Parcelable, Closeable { eventType = parser.next(); } - return builder.build(); + return builder; } @Override @@ -978,10 +984,21 @@ public final class BatteryUsageStats implements Parcelable, Closeable { */ @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 a12606bc2e66..b533225192ba 100644 --- a/core/java/android/os/BatteryUsageStatsQuery.java +++ b/core/java/android/os/BatteryUsageStatsQuery.java @@ -77,6 +77,8 @@ public final class BatteryUsageStatsQuery implements Parcelable { 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 @@ public final class BatteryUsageStatsQuery implements Parcelable { } /** + * 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 97e9f34064ba..ed75491b8e21 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -1111,6 +1111,21 @@ public class Binder implements IBinder { } /** + * 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 @@ public class Binder implements IBinder { } 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 0776cf405cfe..e2a72dd5e385 100644 --- a/core/java/android/os/IpcDataCache.java +++ b/core/java/android/os/IpcDataCache.java @@ -48,6 +48,20 @@ import java.util.concurrent.atomic.AtomicLong; * 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 @@ import java.util.concurrent.atomic.AtomicLong; * 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 cfbf5289931d..a5697fb0e8a8 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 @@ public class SystemVibratorManager extends VibratorManager { 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 @@ public class SystemVibratorManager extends VibratorManager { 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 @@ public class SystemVibratorManager extends VibratorManager { + " 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/ranging/mock/RangingFrameworkInitializer.java b/core/java/android/ranging/mock/RangingFrameworkInitializer.java new file mode 100644 index 000000000000..540f51954a9c --- /dev/null +++ b/core/java/android/ranging/mock/RangingFrameworkInitializer.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.ranging; + +/** +* Mock RangingFrameworkInitializer. +* +* @hide +*/ + +// TODO(b/331206299): Remove this after RANGING_STACK_ENABLED is ramped up to next. +public final class RangingFrameworkInitializer { + private RangingFrameworkInitializer() {} + /** + * @hide + */ + public static void registerServiceWrappers() { + // No-op. + } +} diff --git a/core/java/android/security/OWNERS b/core/java/android/security/OWNERS index c38ee089a5c2..325d2742c2da 100644 --- a/core/java/android/security/OWNERS +++ b/core/java/android/security/OWNERS @@ -10,3 +10,4 @@ per-file Confirmation*.java = file:/keystore/OWNERS 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 88da8ebc3f95..48d7cf768e77 100644 --- a/core/java/android/service/notification/NotificationAssistantService.java +++ b/core/java/android/service/notification/NotificationAssistantService.java @@ -18,6 +18,7 @@ package android.service.notification; 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 @@ public abstract class NotificationAssistantService extends NotificationListenerS = "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 abstract class NotificationAssistantService extends NotificationListenerS 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 c88048c17160..1f341caa8ed3 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 @@ public interface WindowManagerPolicyConstants { int FLAG_VIRTUAL = 0x00000002; int FLAG_INJECTED_FROM_ACCESSIBILITY = POLICY_FLAG_INJECTED_FROM_ACCESSIBILITY; + int FLAG_KEY_GESTURE_TRIGGERED = POLICY_FLAG_KEY_GESTURE_TRIGGERED; int FLAG_INJECTED = 0x01000000; int FLAG_TRUSTED = 0x02000000; int FLAG_FILTERED = 0x04000000; diff --git a/core/java/android/view/accessibility/AccessibilityWindowInfo.java b/core/java/android/view/accessibility/AccessibilityWindowInfo.java index c92593f81558..7b6e070f0008 100644 --- a/core/java/android/view/accessibility/AccessibilityWindowInfo.java +++ b/core/java/android/view/accessibility/AccessibilityWindowInfo.java @@ -880,10 +880,6 @@ public final class AccessibilityWindowInfo implements Parcelable { * @hide */ public static String typeToString(int type) { - if (Flags.enableTypeWindowControl() && type == TYPE_WINDOW_CONTROL) { - return "TYPE_WINDOW_CONTROL"; - } - switch (type) { case TYPE_APPLICATION: { return "TYPE_APPLICATION"; @@ -903,8 +899,12 @@ public final class AccessibilityWindowInfo implements Parcelable { case TYPE_MAGNIFICATION_OVERLAY: { return "TYPE_MAGNIFICATION_OVERLAY"; } - default: + default: { + if (Flags.enableTypeWindowControl() && type == TYPE_WINDOW_CONTROL) { + return "TYPE_WINDOW_CONTROL"; + } return "<UNKNOWN:" + type + ">"; + } } } diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index cc5e583034a5..fbc30ed3d8f5 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 { } 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 c8b7defb5276..702e5e26ffc1 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.Bundle; 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 @@ public class ScreenshotRequest implements Parcelable { 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 @@ public class ScreenshotRequest implements Parcelable { mBitmap = bitmap; mBoundsInScreen = boundsInScreen; mInsets = insets; + mDisplayId = displayId; } ScreenshotRequest(Parcel in) { @@ -77,6 +80,7 @@ public class ScreenshotRequest implements Parcelable { 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 @@ public class ScreenshotRequest implements Parcelable { return mTopComponent; } + public int getDisplayId() { + return mDisplayId; + } + @Override public int describeContents() { return 0; @@ -128,6 +136,7 @@ public class ScreenshotRequest implements Parcelable { dest.writeTypedObject(HardwareBitmapBundler.hardwareBitmapToBundle(mBitmap), 0); dest.writeTypedObject(mBoundsInScreen, 0); dest.writeTypedObject(mInsets, 0); + dest.writeInt(mDisplayId); } @NonNull @@ -161,6 +170,7 @@ public class ScreenshotRequest implements Parcelable { 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 @@ public class ScreenshotRequest implements Parcelable { } return new ScreenshotRequest(mType, mSource, mTopComponent, mTaskId, mUserId, mBitmap, - mBoundsInScreen, mInsets); + mBoundsInScreen, mInsets, mDisplayId); } /** @@ -255,6 +265,16 @@ public class ScreenshotRequest implements Parcelable { 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 5a444bb1d0ff..c364451057bc 100644 --- a/core/jni/android_util_XmlBlock.cpp +++ b/core/jni/android_util_XmlBlock.cpp @@ -83,7 +83,7 @@ static jlong android_content_XmlBlock_nativeCreateParseState(JNIEnv* env, jobjec 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 e9ce71230a82..9821d433500f 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -25,6 +25,7 @@ filegroup { "BinderProxyCountingTestApp/src/**/*.java", "BinderProxyCountingTestService/src/**/*.java", "BinderDeathRecipientHelperApp/src/**/*.java", + "AppThatCallsBinderMethods/src/**/*.kt", ], visibility: ["//visibility:private"], } @@ -144,6 +145,7 @@ android_test { ":BinderProxyCountingTestApp", ":BinderProxyCountingTestService", ":AppThatUsesAppOps", + ":AppThatCallsBinderMethods", ], } diff --git a/core/tests/coretests/AndroidTest.xml b/core/tests/coretests/AndroidTest.xml index b1f1e2c2db05..05ab783c01bb 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 000000000000..dcc0d4f76bf2 --- /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 000000000000..b2f6d7897681 --- /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 000000000000..638cc3b7692f --- /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 000000000000..37c6268164a7 --- /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 000000000000..ffcf178beda4 --- /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 000000000000..791c209e4473 --- /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 005538a4d401..d9e90fa001c5 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.util.DisplayMetrics; 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 @@ public class ResourceFlaggingTest { } @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 @@ public class ResourceFlaggingTest { } @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 @@ public class ResourceFlaggingTest { 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 89acbc7986fb..0ce403efa970 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.Insets; 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 @@ public final class ScreenshotRequestTest { 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 @@ public final class ScreenshotRequestTest { .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 @@ public final class ScreenshotRequestTest { assertTrue("Bitmaps should be equal", out.getBitmap().sameAs(bitmap)); assertEquals(new Rect(10, 10, 60, 60), out.getBoundsInScreen()); assertEquals(Insets.of(2, 3, 4, 5), out.getInsets()); + assertEquals(displayId, out.getDisplayId()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java index a2df22c5468f..40b685c243b4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java @@ -17,6 +17,7 @@ package com.android.wm.shell; import static android.view.Display.DEFAULT_DISPLAY; + import static org.junit.Assume.assumeTrue; import android.content.Context; @@ -45,6 +46,9 @@ public abstract class ShellTestCase { // Disable protolog tool when running the tests from studio ProtoLog.REQUIRE_PROTOLOGTOOL = false; + // Make sure ProtoLog is initialized before any logging occurs. + ProtoLog.init(); + MockitoAnnotations.initMocks(this); final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); diff --git a/libs/appfunctions/tests/Android.bp b/libs/appfunctions/tests/Android.bp new file mode 100644 index 000000000000..6f5eff305d8d --- /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 000000000000..9a7d4602b640 --- /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 000000000000..825121267f17 --- /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 000000000000..1f9fddd3c1ec --- /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 b6476c9d466f..ae46a99f09c8 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -50,6 +50,10 @@ constexpr bool skip_eglmanager_telemetry() { constexpr bool resample_gainmap_regions() { return false; } + +constexpr bool query_global_priority() { + return false; +} } // namespace hwui_flags #endif @@ -110,6 +114,7 @@ bool Properties::clipSurfaceViews = false; bool Properties::hdr10bitPlus = false; bool Properties::skipTelemetry = false; bool Properties::resampleGainmapRegions = false; +bool Properties::queryGlobalPriority = false; int Properties::timeoutMultiplier = 1; @@ -187,6 +192,7 @@ bool Properties::load() { 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 db471527b861..6f84796fb11e 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -346,6 +346,7 @@ public: 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 ab052b902e02..93df47853769 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -129,3 +129,13 @@ flag { 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 e3023937964e..6571d92aeafa 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -44,7 +44,7 @@ namespace uirenderer { 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 @@ static std::array<std::string_view, 21> sEnableExtensions{ 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 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, 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 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, // 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 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, 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 @@ void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions, 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 f0425719ea89..a593ec6f8351 100644 --- a/libs/hwui/renderthread/VulkanManager.h +++ b/libs/hwui/renderthread/VulkanManager.h @@ -152,7 +152,7 @@ private: 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 31f89960836b..ef4c3ef0d321 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.hardware.display.VirtualDisplayConfig; 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 @@ public final class MediaProjection { 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 @@ public final class MediaProjection { 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 @@ public final class MediaProjection { if (surface != null) { builder.setSurface(surface); } + builder.setDisplayIdToMirror(mDisplayId); return createVirtualDisplay(builder, callback, handler); } @@ -234,6 +244,7 @@ public final class MediaProjection { 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 c376f2566cbb..8b283476070c 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 @@ public final class SoundTriggerInstrumentation { * 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 783528711877..3d0c4069e782 100644 --- a/media/java/android/media/soundtrigger/SoundTriggerManager.java +++ b/media/java/android/media/soundtrigger/SoundTriggerManager.java @@ -19,6 +19,7 @@ package android.media.soundtrigger; 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.ClearCallingIdentityContext; 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 final class SoundTriggerManager { 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 @@ public final class SoundTriggerManager { * * @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 @@ public final class SoundTriggerManager { /** * 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 @@ public final class SoundTriggerManager { } /** - * 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 @@ public final class SoundTriggerManager { } /** - * 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 @@ public final class SoundTriggerManager { } /** - * 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 @@ public final class SoundTriggerManager { } /** - * 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 @@ public final class SoundTriggerManager { } /** - * 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 @@ public final class SoundTriggerManager { } /** - * 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 @@ public final class SoundTriggerManager { } /** - * 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 71ecb4c30543..37a0e7915a0a 100644 --- a/packages/SettingsLib/ActionButtonsPreference/Android.bp +++ b/packages/SettingsLib/ActionButtonsPreference/Android.bp @@ -19,6 +19,7 @@ android_library { 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 000000000000..3e73ebd4880e --- /dev/null +++ b/packages/SettingsLib/ActionButtonsPreference/res/layout-v35/settingslib_expressive_action_buttons.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/settingslib_expressive_space_extrasmall4" + android:paddingHorizontal="@dimen/settingslib_expressive_space_extrasmall4" + android:orientation="horizontal"> + + <LinearLayout + android:id="@+id/action1" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button1" + style="@style/SettingsLibActionButton.Expressive" + android:layout_width="@dimen/settingslib_expressive_space_large3" + android:layout_height="@dimen/settingslib_expressive_space_medium5" + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no"/> + <TextView + android:id="@+id/text1" + style="@style/SettingsLibActionButton.Expressive.Label" + android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3" + android:importantForAccessibility="no"/> + + </LinearLayout> + + <LinearLayout + android:id="@+id/action2" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button2" + style="@style/SettingsLibActionButton.Expressive" + android:layout_width="@dimen/settingslib_expressive_space_large3" + android:layout_height="@dimen/settingslib_expressive_space_medium5" + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no"/> + <TextView + android:id="@+id/text2" + style="@style/SettingsLibActionButton.Expressive.Label" + android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3" + android:importantForAccessibility="no"/> + + </LinearLayout> + + <LinearLayout + android:id="@+id/action3" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button3" + style="@style/SettingsLibActionButton.Expressive" + android:layout_width="@dimen/settingslib_expressive_space_large3" + android:layout_height="@dimen/settingslib_expressive_space_medium5" + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no"/> + <TextView + android:id="@+id/text3" + style="@style/SettingsLibActionButton.Expressive.Label" + android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3" + android:importantForAccessibility="no"/> + + </LinearLayout> + + <LinearLayout + android:id="@+id/action4" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/button4" + style="@style/SettingsLibActionButton.Expressive" + android:layout_width="@dimen/settingslib_expressive_space_large3" + android:layout_height="@dimen/settingslib_expressive_space_medium5" + android:layout_gravity="center_horizontal" + android:importantForAccessibility="no"/> + <TextView + android:id="@+id/text4" + style="@style/SettingsLibActionButton.Expressive.Label" + android:layout_marginTop="@dimen/settingslib_expressive_space_extrasmall3" + android:importantForAccessibility="no"/> + </LinearLayout> +</LinearLayout> diff --git a/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml b/packages/SettingsLib/ActionButtonsPreference/res/values-v35/styles_expressive.xml new file mode 100644 index 000000000000..cc948a670382 --- /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 5dc11cfc0392..b2861826a103 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.AttributeSet; 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 androidx.preference.PreferenceViewHolder; 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 @@ public class ActionButtonsPreference extends Preference { } 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 @@ public class ActionButtonsPreference extends Preference { 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 @@ public class ActionButtonsPreference extends Preference { mVisibleButtonInfos.add(mButton4Info); } - final boolean isRtl = getContext().getResources().getConfiguration() - .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - switch (mVisibleButtonInfos.size()) { - case SINGLE_BUTTON_STYLE : - if (isRtl) { - setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1); - } else { - setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1); - } - break; - case TWO_BUTTONS_STYLE : - if (isRtl) { - setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2); - } else { - setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2); - } - break; - case THREE_BUTTONS_STYLE : - if (isRtl) { - setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3); - } else { - setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3); - } - break; - case FOUR_BUTTONS_STYLE : - if (isRtl) { - setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4); - } else { - setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4); - } - break; - default: - Log.e(TAG, "No visible buttons info, skip background settings."); - break; - } + if (!SettingsThemeHelper.isExpressiveTheme(getContext())) { + final boolean isRtl = getContext().getResources().getConfiguration() + .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + switch (mVisibleButtonInfos.size()) { + case SINGLE_BUTTON_STYLE : + if (isRtl) { + setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1); + } else { + setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle1); + } + break; + case TWO_BUTTONS_STYLE : + if (isRtl) { + setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2); + } else { + setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle2); + } + break; + case THREE_BUTTONS_STYLE : + if (isRtl) { + setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3); + } else { + setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle3); + } + break; + case FOUR_BUTTONS_STYLE : + if (isRtl) { + setupRtlBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4); + } else { + setupBackgrounds(mVisibleButtonInfos, mBtnBackgroundStyle4); + } + break; + default: + Log.e(TAG, "No visible buttons info, skip background settings."); + break; + } - setupDivider1(); - setupDivider2(); - setupDivider3(); + setupDivider1(); + setupDivider2(); + setupDivider3(); + } } private void setupBackgrounds( @@ -509,23 +533,45 @@ public class ActionButtonsPreference extends Preference { static class ButtonInfo { private Button mButton; + private TextView mTextView; + private LinearLayout mActionLayout; private CharSequence mText; private Drawable mIcon; private View.OnClickListener mListener; private boolean mIsEnabled = true; private boolean mIsVisible = true; + private boolean mIsExpressive = false; void setUpButton() { - mButton.setText(mText); - mButton.setOnClickListener(mListener); - mButton.setEnabled(mIsEnabled); - mButton.setCompoundDrawablesWithIntrinsicBounds( - null /* left */, mIcon /* top */, null /* right */, null /* bottom */); + if (mIsExpressive) { + mTextView.setText(mText); + if (mButton instanceof MaterialButton) { + ((MaterialButton) mButton).setIcon(mIcon); + } + mButton.setEnabled(mIsEnabled); + mActionLayout.setOnClickListener(mListener); + mActionLayout.setEnabled(mIsEnabled); + mActionLayout.setContentDescription(mText); + } else { + mButton.setText(mText); + mButton.setCompoundDrawablesWithIntrinsicBounds( + null /* left */, mIcon /* top */, null /* right */, null /* bottom */); + mButton.setOnClickListener(mListener); + mButton.setEnabled(mIsEnabled); + } if (shouldBeVisible()) { mButton.setVisibility(View.VISIBLE); + if (mIsExpressive) { + mTextView.setVisibility(View.VISIBLE); + mActionLayout.setVisibility(View.VISIBLE); + } } else { mButton.setVisibility(View.GONE); + if (mIsExpressive) { + mTextView.setVisibility(View.GONE); + mActionLayout.setVisibility(View.GONE); + } } } diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index 0cb85d8638b0..d4851e1ad698 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -32,6 +32,7 @@ android_library { "SettingsLibBannerMessagePreference", "SettingsLibBarChartPreference", "SettingsLibButtonPreference", + "SettingsLibBulletPreference", "SettingsLibCollapsingToolbarBaseActivity", "SettingsLibDeviceStateRotationLock", "SettingsLibDisplayUtils", @@ -53,6 +54,7 @@ android_library { "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 000000000000..3ea0b2b4851e --- /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 000000000000..c7495eff14d2 --- /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 000000000000..030f02430c02 --- /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 000000000000..3f37f6cc00a5 --- /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 000000000000..45e2e9aaea4b --- /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 000000000000..ccbe20e1c61f --- /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 000000000000..b881c57e2f53 --- /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 000000000000..6221659388d1 --- /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 000000000000..d58c2c2eeb23 --- /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 000000000000..ca1904a15b2f --- /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 f46f110e65b8..feacecbd5d0c 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 android.widget.Toolbar; 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 @@ public class CollapsingToolbarAppCompatActivity extends AppCompatActivity { 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 16ed5a8079fc..0f9d94e9cc3d 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 android.widget.Toolbar; 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 @@ public class CollapsingToolbarBaseActivity extends FragmentActivity { 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 @@ public class CollapsingToolbarBaseActivity extends FragmentActivity { 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 2ab2abd03c87..01ecb66fed1a 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 static android.text.Layout.HYPHENATION_FREQUENCY_NORMAL_FAST; 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.annotation.Nullable; 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 @@ public class CollapsingToolbarDelegate { private boolean mUseCollapsingToolbar; + private boolean mIsExpressiveTheme; + public CollapsingToolbarDelegate(@NonNull HostCallback hostCallback, boolean useCollapsingToolbar) { mHostCallback = hostCallback; @@ -103,11 +108,16 @@ public class CollapsingToolbarDelegate { 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 @@ public class CollapsingToolbarDelegate { 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 @@ public class CollapsingToolbarDelegate { 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 @@ public class CollapsingToolbarDelegate { 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 f70add9cc62d..51d7504f136d 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.appcompat.app.AppCompatActivity; 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 @@ public class CollapsingCoordinatorLayout extends CoordinatorLayout { } 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 @@ public class CollapsingCoordinatorLayout extends CoordinatorLayout { 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 @@ public class CollapsingCoordinatorLayout extends CoordinatorLayout { 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 000000000000..3999e762ea9e --- /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 000000000000..4425ef08d6e3 --- /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 e6f61a8400c4..106802e9d1d1 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; 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.LinearLayout; import android.widget.TextView; import androidx.annotation.ColorInt; +import androidx.annotation.RequiresApi; import com.android.settingslib.widget.mainswitch.R; @@ -52,6 +54,7 @@ public class MainSwitchBar extends LinearLayout implements OnCheckedChangeListen private int mBackgroundActivatedColor; protected TextView mTextView; + protected TextView mSummaryView; protected CompoundButton mSwitch; private final View mFrameView; @@ -71,7 +74,11 @@ public class MainSwitchBar extends LinearLayout implements OnCheckedChangeListen 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 @@ public class MainSwitchBar extends LinearLayout implements OnCheckedChangeListen 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 @@ public class MainSwitchBar extends LinearLayout implements OnCheckedChangeListen 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 @@ public class MainSwitchBar extends LinearLayout implements OnCheckedChangeListen } /** + * 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 @@ public class MainSwitchBar extends LinearLayout implements OnCheckedChangeListen 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 b294d4e3ad19..d895c874d95e 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 @@ package com.android.settingslib.widget; 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 @@ public class MainSwitchPreference extends TwoStatePreference implements OnChecke 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 @@ public class MainSwitchPreference extends TwoStatePreference implements OnChecke } @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 000000000000..65d0e4c55508 --- /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 000000000000..56d781834a8a --- /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 000000000000..95661c9503fa --- /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 000000000000..d382dadebf96 --- /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 000000000000..8f0311165ca0 --- /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 000000000000..a6885a4ac358 --- /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 000000000000..9216c9615aaa --- /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 dda7517cc1c3..952562e3d8ea 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 000000000000..433d26445c4d --- /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 fedcc77ed6b9..4e23b6562e3e 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 000000000000..f93e1b975eb2 --- /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 2320aab8f459..0542c510fa63 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 04ae80e72401..816433c1a18b 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 d01c0b90481c..272dc2d1958f 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.2-bin.zip Binary files differindex 50432f3369c6..45f0424639f2 100644 --- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10-bin.zip +++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.10.2-bin.zip diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties index 9a7f4b60b773..1c25e97452eb 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 790aa9ff8d3f..8f8275bed702 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -58,12 +58,12 @@ dependencies { 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 @@ tasks.register<JacocoReport>("coverageReport") { // 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 000000000000..4fc00bdbfee0 --- /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 000000000000..51b0ab86c835 --- /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 000000000000..f42b4415c39e --- /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 000000000000..c0b195cc1f74 --- /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 000000000000..e981eccf2be2 --- /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 000000000000..9b1ccef9dadf --- /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 712ddc8aea4b..5eeb49a0b398 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 @@ public class TestModeBuilder { 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 4324d463c276..d9230ec67461 100644 --- a/packages/SystemUI/animation/lib/Android.bp +++ b/packages/SystemUI/animation/lib/Android.bp @@ -33,6 +33,20 @@ java_library { ], } +// 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 000000000000..64bedd347d7a --- /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 000000000000..c1a3e84e1d80 --- /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 000000000000..1788abff4400 --- /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 000000000000..0f37d7af22b5 --- /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 000000000000..287e53b906ba --- /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 bfeaf928dfe8..0f6e6a7c4383 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.LockscreenSceneBlueprintModul 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 dagger.multibindings.IntoSet 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 @@ interface LockscreenSceneModule { @Provides @SysUISingleton @KeyguardRootView - fun viewProvider( - configurator: Provider<KeyguardViewConfigurator>, - ): () -> View { + fun viewProvider(configurator: Provider<KeyguardViewConfigurator>): () -> View { return { configurator.get().getKeyguardRootView() } } @@ -67,10 +61,16 @@ interface LockscreenSceneModule { @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 dbe75382556f..5c5514aec03e 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.domain.interactor.KeyguardClockInteractor 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 @@ import com.android.systemui.lifecycle.rememberViewModel */ 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 @@ class LockscreenContent( } @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 @@ class LockscreenContent( } 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 000000000000..4279be3efad0 --- /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 fe4a65b8bbd0..2066c9314bc7 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.ElementKey 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.Notificati 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 @@ fun SceneScope.ConstrainedNotificationStack( NotificationPlaceholder( stackScrollView = stackScrollView, viewModel = viewModel, + useStackBounds = { shouldUseLockscreenStackBounds(layoutState.transitionState) }, modifier = Modifier.fillMaxSize(), ) HeadsUpNotificationSpace( @@ -363,7 +366,6 @@ fun SceneScope.NotificationScrollingStack( snapshotFlow { syntheticScroll.value } .collect { delta -> scrollNotificationStack( - scope = coroutineScope, delta = delta, animate = false, scrimOffset = scrimOffset, @@ -383,7 +385,6 @@ fun SceneScope.NotificationScrollingStack( // 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 @@ fun SceneScope.NotificationScrollingStack( scrimCornerRadius, screenCornerRadius, { expansionFraction }, - shouldPunchHoleBehindScrim, + shouldAnimateScrimCornerRadius( + layoutState, + shouldPunchHoleBehindScrim, + ), ) .let { scrimRounding.value.toRoundedCornerShape(it) } clip = true @@ -514,6 +518,9 @@ fun SceneScope.NotificationScrollingStack( NotificationPlaceholder( stackScrollView = stackScrollView, viewModel = viewModel, + useStackBounds = { + !shouldUseLockscreenStackBounds(layoutState.transitionState) + }, modifier = Modifier.notificationStackHeight( view = stackScrollView, @@ -600,6 +607,7 @@ fun SceneScope.NotificationStackCutoffGuideline( private fun SceneScope.NotificationPlaceholder( stackScrollView: NotificationScrollView, viewModel: NotificationsPlaceholderViewModel, + useStackBounds: () -> Boolean, modifier: Modifier = Modifier, ) { Box( @@ -609,21 +617,26 @@ private fun SceneScope.NotificationPlaceholder( .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 @@ private suspend fun scrollNotificationStack( 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 suspend fun scrollNotificationStack( } } +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 df101c558dff..dc9e267cb0e1 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 @@ fun SceneContainer( 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 @@ fun SceneContainer( // Render the scene. with(scene) { this@scene.Content( - modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize(), + modifier = Modifier.element(sceneKey.rootElementKey).fillMaxSize() ) } } @@ -152,7 +153,7 @@ fun SceneContainer( 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 @@ fun SceneContainer( } 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 58fbf430b20c..303a6f0d942a 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 @@ val SceneContainerTransitions = transitions { 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 ac54896c5031..4c0efd2047ff 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.CubicBezierEasing 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 7f2ee2a8351a..db0fe3e3f79d 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 @@ private fun SceneScope.SingleShade( 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 f38a31026664..9891025ad7d3 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 @@ internal class DraggableHandlerImpl( 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 cec888380513..6e89814a2dc2 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 @@ import androidx.compose.ui.unit.LayoutDirection * @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 @@ fun SceneTransitionLayout( 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 @@ fun SceneTransitionLayout( modifier, swipeSourceDetector, swipeDetector, + gestureFilter, transitionInterceptionThreshold, onLayoutImpl = null, builder, @@ -616,6 +621,7 @@ internal fun SceneTransitionLayoutForTesting( 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 @@ internal fun SceneTransitionLayoutForTesting( 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 65c404387734..9e7be37523f4 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.runtime.snapshots.SnapshotStateMap 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 @@ internal class SceneTransitionLayoutImpl( * 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 54ee78366875..f758102fee47 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 @@ interface SwipeDetector { 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 fca92ca804fa..b64b8be90ad2 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 @@ class DraggableHandlerTest { val transitionInterceptionThreshold = 0.05f + var gestureFilter: (startedPosition: Offset) -> Boolean = DefaultGestureFilter + private val layoutImpl = SceneTransitionLayoutImpl( state = layoutState, @@ -120,6 +122,7 @@ class DraggableHandlerTest { // 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 @@ class DraggableHandlerTest { } @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 a08fbbf75805..fa304c99ecc9 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 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu @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 88a1df147489..3388c75bcf78 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.flags.EnableSceneContainer 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 @@ import org.junit.runner.RunWith @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 abd1e2c7df82..8c7ec4743e7c 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.flags.EnableSceneContainer 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 @@ import org.junit.runner.RunWith @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 a0cafcbd5ad1..c9e958dd1cc0 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 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { 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 000000000000..efde1ecec512 --- /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 a0bb01797f2c..e60e742e9917 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.power.domain.interactor.powerInteractor 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 @@ class SceneContainerViewModelTest : SysuiTestCase() { 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 @@ class SceneContainerViewModelTest : SysuiTestCase() { 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 d163abf66b05..19ac0cf40160 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 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun anyExpansion_shadeGreater() = - testScope.runTest() { + testScope.runTest { // WHEN shade is more expanded than QS shadeTestUtil.setShadeAndQsExpansion(.5f, 0f) runCurrent() @@ -298,7 +298,7 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun anyExpansion_qsGreater() = - testScope.runTest() { + testScope.runTest { // WHEN qs is more expanded than shade shadeTestUtil.setShadeAndQsExpansion(0f, .5f) runCurrent() @@ -308,6 +308,36 @@ class ShadeInteractorImplTest(flags: FlagsParameterization) : SysuiTestCase() { } @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 109cd05368e6..4592b60e7c2c 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 com.google.common.truth.Truth.assertThat 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 @@ class ShadeInteractorLegacyImplTest : SysuiTestCase() { // 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 f6fe667ff813..eb8ea8ba15cf 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.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.activityStarter import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.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 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { 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 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(DualShade.FLAG_NAME) fun onSystemIconContainerClicked_locked_collapsesShadeToLockscreen() = testScope.runTest { setDeviceEntered(false) @@ -108,6 +114,25 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { } @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 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { 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 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { 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 000000000000..f9b77697b767 --- /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 26e1a4d9e961..d12d6f6b885d 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.data.repository.setActiveNoti 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 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas 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 @@ class NotificationListViewModelTest(flags: FlagsParameterization) : SysuiTestCas } @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 2c8cc1ae6ba4..305367213571 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.kotlin.JavaAdapter 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 @@ class HeadsUpManagerPhoneTest(flags: FlagsParameterization) : BaseHeadsUpManager @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 0f6dc0723f42..c5ccf9e6a1d1 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 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.SysuiTestCase 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 @@ class ZenModeInteractorTest : SysuiTestCase() { 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 248e61100cad..e26b85547d3a 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 24b657943e37..75389b136a3b 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 8b5a09b3d9fd..2c026c0bb5ce 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 @@ constructor( prepareToPerformAction() returnToCall() }, - onLongClick = null + onLongClick = null, ) } @@ -115,15 +115,15 @@ constructor( 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 @@ constructor( 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 0b775ab486bd..820c102e81d8 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.keyguard.MigrateClocksToBlueprint 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 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha 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/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 493afde96aff..aa1873c7ad41 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -2298,9 +2298,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + public boolean onScroll(@Nullable MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { if (distanceY < 0 && distanceY > distanceX + && e1 != null && e1.getY() <= mStatusBarWindowController.getStatusBarHeight()) { // Downwards scroll from top openShadeAndDismiss(); @@ -2310,9 +2312,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + public boolean onFling(@Nullable MotionEvent e1, MotionEvent e2, + float velocityX, float velocityY) { if (velocityY > 0 && Math.abs(velocityY) > Math.abs(velocityX) + && e1 != null && e1.getY() <= mStatusBarWindowController.getStatusBarHeight()) { // Downwards fling from top openShadeAndDismiss(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java index 68a252b2caba..654c76359505 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardSliceProvider.java @@ -515,7 +515,13 @@ public class KeyguardSliceProvider extends SliceProvider implements } protected void notifyChange() { - mBgHandler.post(() -> mContentResolver.notifyChange(mSliceUri, null /* observer */)); + mBgHandler.post(() -> { + try { + mContentResolver.notifyChange(mSliceUri, null /* observer */); + } catch (Exception e) { + Log.e(TAG, "Error on mContentResolver.notifyChange()", e); + } + }); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 416eabae78eb..063adc834f30 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.NotificationShadeWindowView 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 @@ constructor( 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 @@ constructor( private fun createLockscreen( context: Context, viewModelFactory: LockscreenContentViewModel.Factory, + notificationScrimViewModelFactory: NotificationLockscreenScrimViewModel.Factory, blueprints: Set<@JvmSuppressWildcards LockscreenSceneBlueprint>, ): View { val sceneBlueprints = @@ -222,6 +225,8 @@ constructor( 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 5be225c718ea..219e45c36b50 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 @@ class NotificationsShadeOverlayContentViewModel 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 c8079600e980..ba0d9384c7a4 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 @@ constructor( savedInstanceState: Bundle?, ): View { val context = inflater.context - return 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, + 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, + ) + } + .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 class ExpansionTransition(currentProgress: Float) : } private const val EDIT_MODE_TIME_MILLIS = 500 + +/** + * Ignore touches below the value returned by [clippingTopProvider], when clipping is enabled, as + * per [clippingEnabledProvider]. + */ +private class FrameLayoutTouchPassthrough( + context: Context, + private val clippingEnabledProvider: () -> Boolean, + private val clippingTopProvider: () -> Int, +) : FrameLayout(context) { + override fun isTransformedTouchPointInView( + x: Float, + y: Float, + child: View?, + outLocalPoint: PointF?, + ): Boolean { + return if (clippingEnabledProvider() && y + translationY > clippingTopProvider()) { + false + } else { + super.isTransformedTouchPointInView(x, y, child, outLocalPoint) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt index 7c62e5995ce8..e0f0b6aa8919 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt @@ -20,14 +20,18 @@ import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size +import androidx.compose.foundation.systemGestureExclusion import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.ResizingDotSize /** @@ -52,14 +56,18 @@ fun ResizingHandle( // not receive the touch input accidentally. val minTouchTargetSize = LocalMinimumInteractiveComponentSize.current Box( - Modifier.size(minTouchTargetSize).pointerInput(Unit) { - detectHorizontalDragGestures( - onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) }, - onDragStart = { tileWidths()?.let { selectionState.onResizingDragStart(it) } }, - onDragEnd = selectionState::onResizingDragEnd, - onDragCancel = selectionState::onResizingDragEnd, - ) - } + Modifier.size(minTouchTargetSize) + .systemGestureExclusion { Rect(Offset.Zero, it.size.toSize()) } + .pointerInput(Unit) { + detectHorizontalDragGestures( + onHorizontalDrag = { _, offset -> selectionState.onResizingDrag(offset) }, + onDragStart = { + tileWidths()?.let { selectionState.onResizingDragStart(it) } + }, + onDragEnd = selectionState::onResizingDragEnd, + onDragCancel = selectionState::onResizingDragEnd, + ) + } ) { ResizingDot(transition = transition, modifier = Modifier.align(Alignment.Center)) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 6a8cc1715aca..4f3ea8331a17 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id, resources.getString( R.string.accessibility_tile_disabled_by_policy_action_description - ) + ), ) ) } else { @@ -591,7 +588,7 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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 3b97d820e6a8..7c8fbeaec0d5 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 @@ import dagger.assisted.AssistedInject 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 ac49e91c777c..559c2637ed4f 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 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } 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 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis 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 000000000000..a8d077777121 --- /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 000000000000..4cee874f5f8e --- /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 b9f57f2f31d5..3c6d858092af 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 @@ object TransitionKeys { * 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 7f35d7385aa7..a7e7d8bb34dc 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 @@ object SceneWindowRootViewBinder { 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 @@ object SceneWindowRootViewBinder { PlatformTheme { ScreenDecorProvider( displayCutout = displayCutoutFromWindowInsets(scope, context, windowInsets), - screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) + screenCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context), ) { SceneContainer( viewModel = viewModel, @@ -205,9 +207,7 @@ object SceneWindowRootViewBinder { ): ComposeView { return ComposeView(context).apply { setContent { - AlternateBouncer( - alternateBouncerDependencies = alternateBouncerDependencies, - ) + AlternateBouncer(alternateBouncerDependencies = alternateBouncerDependencies) } } } @@ -234,14 +234,7 @@ object SceneWindowRootViewBinder { 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 000000000000..a1d915a658ec --- /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 af1f5a716961..0bf2d499721b 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 @@ package com.android.systemui.scene.ui.viewmodel 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 com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificat 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 @@ constructor( 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 @@ constructor( }, ) + 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 @@ constructor( } ) - 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 @@ constructor( } } + /** + * 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 @@ constructor( @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 361226a4df18..6c99282bdcdd 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.SysUISingleton 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 @@ constructor( } } + @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 @@ constructor( } } + @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 @@ constructor( animateCollapseShade() } + @Deprecated("Deprecated in Java") override fun closeShadeIfOpen(): Boolean { if (shadeInteractor.isAnyExpanded.value) { commandQueue.animateCollapsePanels( @@ -155,6 +164,7 @@ constructor( animateCollapseShadeForcedDelayed() } + @Deprecated("Deprecated in Java") override fun collapseShade(animate: Boolean) { if (animate) { animateCollapseShade() @@ -163,13 +173,14 @@ constructor( } } + @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 @@ constructor( } } + @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 @@ constructor( } } + @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 b046c50b05d3..a3f2c64f6909 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 @@ import kotlinx.coroutines.flow.stateIn /** 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 @@ interface BaseShadeInteractor { */ 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 @@ interface BaseShadeInteractor { */ 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 @@ interface BaseShadeInteractor { 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 expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey? = null) + + /** + * Triggers the expansion (opening) of the quick settings shade. If it is already expanded, this + * has no effect. + */ + 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 expandNotificationShade(loggingReason: String) + fun collapseQuickSettingsShade( + loggingReason: String, + transitionKey: TransitionKey? = null, + bypassNotificationsShade: Boolean = false, + ) /** - * Triggers the expansion (opening) of the quick settings shade. If the quick settings shade is - * already open, this has no effect. + * 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 expandQuickSettingsShade(loggingReason: String) + 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 fb1482890b87..322fca39a1df 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 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { 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 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { 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 3eab02ad30d5..949d2aa36bf3 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 @@ constructor( 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 df094864a71b..0902c3936661 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 @@ constructor( 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 81bf712f21e5..765810810bb8 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.ContentKey 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 @@ constructor( } } - 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 = 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 = Scenes.QuickSettings, + 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 0fb379017be9..ea76ac4b0f83 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.Background 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 @@ constructor( @Deprecated("Use ShadeInteractor instead") override fun expandToNotifications() { - shadeInteractor.expandNotificationShade( + shadeInteractor.expandNotificationsShade( loggingReason = "ShadeLockscreenInteractorImpl.expandToNotifications" ) } @@ -71,17 +70,11 @@ constructor( } 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 @@ constructor( 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 caa45137ed98..c838c378965f 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 @@ interface ShadeModeInteractor { 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 a154e91feca1..bd4ed5b45dc7 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.plugins.ActivityStarter 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 @@ import kotlinx.coroutines.launch 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 @@ constructor( map = { intent, _ -> intent.action == Intent.ACTION_TIMEZONE_CHANGED || intent.action == Intent.ACTION_LOCALE_CHANGED - } + }, ) .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) } .launchIn(this) @@ -152,10 +149,9 @@ constructor( /** 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 @@ constructor( 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 6907eefa8b56..1c840e08f748 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 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements 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 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements 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 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements 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 index ec3c7d0d6de4..0f93b5d1ea12 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 0bbde21ba6a5..82ce31bfee2d 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 android.util.ArrayMap; 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 @@ public class MediaCoordinator implements Coordinator { 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 @@ public class MediaCoordinator implements Coordinator { 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 @@ public class MediaCoordinator implements Coordinator { // 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 @@ public class MediaCoordinator implements Coordinator { } }; - 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 fe59d732cceb..5ff5d2d9a7e5 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 @@ private const val TAG = "RemoteInputCoordinator" /** * 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 const val REMOTE_INPUT_EXTENDER_RELEASE_DELAY: Long = 200 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 @@ class RemoteInputCoordinator @Inject constructor( if (lifetimeExtensionRefactor()) { pipeline.addNotificationLifetimeExtender(mRemoteInputActiveExtender) } else { - mRemoteInputLifetimeExtenders.forEach { - pipeline.addNotificationLifetimeExtender(it) - } + mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) } } mNotifUpdater = pipeline.getInternalNotifUpdater(TAG) pipeline.addCollectionListener(mCollectionListener) @@ -105,64 +101,85 @@ class RemoteInputCoordinator @Inject constructor( * 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 @@ class RemoteInputCoordinator @Inject constructor( // 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 @@ class RemoteInputCoordinator @Inject constructor( 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 @@ class RemoteInputCoordinator @Inject constructor( @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 @@ class RemoteInputCoordinator @Inject constructor( @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 000000000000..f1fc2751d11f --- /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 850e9447beea..73477da247f4 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.annotation.StringRes; 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 android.widget.TextView; 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 @@ public class EmptyShadeView extends StackScrollerDecorView { 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 @@ public class EmptyShadeView extends StackScrollerDecorView { 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 @@ public class EmptyShadeView extends StackScrollerDecorView { } } + /** 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 @@ public class EmptyShadeView extends StackScrollerDecorView { 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 000000000000..102a11c2314c --- /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 000000000000..d5417e7ae8f6 --- /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 291dc132686b..cd228e7872c1 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 @@ public abstract class StackScrollerDecorView extends ExpandableView { } /** + * 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 @@ public abstract class StackScrollerDecorView extends ExpandableView { } 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 1431b28bf794..d246b04b7957 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 @@ public class AmbientState implements Dumpable { 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 @@ public class AmbientState implements Dumpable { 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 cd3516dadbad..0a44a2bc9d93 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.NotificationUtils; 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 @@ public class NotificationStackScrollLayout protected void onFinishInflate() { super.onFinishInflate(); - inflateEmptyShadeView(); + if (!ModesEmptyShadeFix.isEnabled()) { + inflateEmptyShadeView(); + } if (!FooterViewRefactor.isEnabled()) { inflateFooterView(); } @@ -729,7 +732,9 @@ public class NotificationStackScrollLayout inflateFooterView(); updateFooter(); } - inflateEmptyShadeView(); + if (!ModesEmptyShadeFix.isEnabled()) { + inflateEmptyShadeView(); + } mSectionsManager.reinflateViews(); } @@ -4835,6 +4840,8 @@ public class NotificationStackScrollLayout /** 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 @@ public class NotificationStackScrollLayout @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 class NotificationStackScrollLayout public boolean isEmptyShadeViewVisible() { SceneContainerFlag.assertInLegacyMode(); + if (mEmptyShadeView == null) { + return false; + } return mEmptyShadeView.isVisible(); } @@ -5330,6 +5342,19 @@ public class NotificationStackScrollLayout 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 class NotificationStackScrollLayout 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 @@ public class NotificationStackScrollLayout } 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 dad6894a43ce..9c5fecf0338e 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 @@ public class NotificationStackScrollLayoutController implements Dumpable { 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 9c0fd0e844b4..e0b0ccd9e840 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 @@ public class StackScrollAlgorithm { 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 f6722a4ccff0..c0f1a5619140 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 @@ class NotificationPlaceholderRepository @Inject constructor() { /** 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 756cd87970a4..32e092bcdf4d 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 @@ constructor( /** 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 @@ constructor( 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 @@ constructor( val shouldCloseGuts: Flow<Boolean> = combine( sceneInteractor.isSceneContainerUserInputOngoing, - viewHeightRepository.isCurrentGestureInGuts + viewHeightRepository.isCurrentGestureInGuts, ) { isUserInputOngoing, isCurrentGestureInGuts -> isUserInputOngoing && !isCurrentGestureInGuts } @@ -109,6 +112,11 @@ constructor( 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 41c02934efa6..0113e361b3d7 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 @@ interface NotificationScrollView { /** 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 dc9615c25ada..3dad32662893 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.ConfigurationState 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.NotificationShelf 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.statusbar.notification.ui.viewbinder.HeadsUpNotifica 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 @@ constructor( fun bindWhileAttached( view: NotificationStackScrollLayout, - viewController: NotificationStackScrollLayoutController + viewController: NotificationStackScrollLayoutController, ) { val shelf = LayoutInflater.from(view.context) @@ -103,7 +110,13 @@ constructor( 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 @@ constructor( } 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 @@ constructor( footerView, footerViewModel, parentView, - hasNonClearableSilentNotifications + hasNonClearableSilentNotifications, ) } } @@ -163,13 +171,13 @@ constructor( 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 @@ constructor( 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 @@ constructor( footerView.setVisible( /* visible = */ animatedVisibility.value, /* animate = */ animatedVisibility.isAnimating, - ) + ) { + animatedVisibility.stopAnimating() + } } } } else { @@ -211,20 +213,71 @@ constructor( 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 @@ constructor( private fun clearSilentNotifications( view: NotificationStackScrollLayout, closeShade: Boolean, - hideSilentSection: Boolean + hideSilentSection: Boolean, ) { view.clearSilentNotifications(closeShade, hideSilentSection) } @@ -270,11 +323,7 @@ constructor( 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 2e37dead8787..99ff678d10dd 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 @@ constructor( 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 4e2a46d78a5d..935e2a37b13c 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.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.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.flowOn 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 @@ constructor( } val shouldShowEmptyShadeView: Flow<Boolean> by lazy { + ModesEmptyShadeFix.assertInLegacyMode() if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { flowOf(false) } else { @@ -114,6 +117,45 @@ constructor( } } + 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 @@ constructor( userSetupInteractor.isUserSetUp, notificationStackInteractor.isShowingOnLockscreen, shadeInteractor.isQsFullscreen, - remoteInputInteractor.isRemoteInputActive + remoteInputInteractor.isRemoteInputActive, ) { hasNotifications, isUserSetUp, @@ -193,7 +235,7 @@ constructor( combine( shadeInteractor.isShadeFullyExpanded, shadeInteractor.isShadeTouchable, - ::Pair + ::Pair, ) .onStart { emit(Pair(false, false)) } ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) -> @@ -263,7 +305,7 @@ constructor( combine( shadeInteractor.isShadeFullyExpanded, shadeInteractor.isShadeTouchable, - ::Pair + ::Pair, ) .onStart { emit(Pair(false, false)) } ) { visibilityChange, (isShadeFullyExpanded, animationsEnabled) -> @@ -283,29 +325,7 @@ constructor( 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 000000000000..84aa997cf0e1 --- /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 5b2e02d446cf..cd9c07e38b3a 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 @@ constructor( 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 @@ constructor( ): 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 @@ constructor( .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 @@ constructor( 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 caf09a3b638e..674cbb7c3c0d 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( ) } } - stateAfter += getStateStr() - headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled = true, key, stateAfter) + outcome += getStateStr() + headsUpManagerLogger.logAvalancheUpdate(caller, isEnabled, key, outcome) } @VisibleForTesting @@ -194,40 +181,32 @@ constructor( 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 @@ constructor( // 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 @@ constructor( } 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 30524a5f26d5..f37393ac6729 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { * 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 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } }; 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 41112cb5f8e7..600270c7189a 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 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { caller: String, isEnabled: Boolean, notifEntryKey: String, - stateAfter: String + outcome: String ) { buffer.log( TAG, @@ -60,7 +60,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { { 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 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { caller: String, isEnabled: Boolean, notifEntryKey: String, - stateAfter: String + outcome: String ) { buffer.log( TAG, @@ -79,7 +79,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { { 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 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { str1 = entry.logKey str2 = reason ?: "unknown" }, - { "$str2 => request: cancelAutoRemovalCallbacks: $str1" } + { "request: cancel auto remove of $str1 reason: $str2" } ) } @@ -148,7 +148,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { str1 = entry.logKey str2 = reason ?: "unknown" }, - { "$str2 => cancel auto remove: $str1" } + { "cancel auto remove of $str1 reason: $str2" } ) } @@ -161,7 +161,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { str2 = reason bool1 = isWaiting }, - { "request: $str2 => removeEntry: $str1 isWaiting: $isWaiting" } + { "request: $str2 => remove entry $str1 isWaiting: $isWaiting" } ) } @@ -174,7 +174,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { str2 = reason bool1 = isWaiting }, - { "$str2 => removeEntry: $str1 isWaiting: $isWaiting" } + { "$str2 => remove entry $str1 isWaiting: $isWaiting" } ) } @@ -216,12 +216,12 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { 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 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { bool1 = alert bool2 = hasEntry }, - { "request: update notif $str1 alert: $bool1 hasEntry: $bool2" } + { "request: update notification $str1 alert: $bool1 hasEntry: $bool2" } ) } @@ -246,7 +246,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { bool1 = alert bool2 = hasEntry }, - { "update notif $str1 alert: $bool1 hasEntry: $bool2" } + { "update notification $str1 alert: $bool1 hasEntry: $bool2" } ) } @@ -281,7 +281,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { bool1 = isPinned str2 = reason }, - { "$str2 => setEntryPinned[$bool1]: $str1" } + { "$str2 => set entry pinned $str1 pinned: $bool1" } ) } @@ -290,7 +290,7 @@ constructor(@NotificationHeadsUpLog private val buffer: LogBuffer) { 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 ba45942177a2..daba1099c49d 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.content.Context 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.settingslib.notification.modes.ZenMode 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.CoroutineDispatcher 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 @@ constructor( 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 @@ constructor( 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 index 24f3a29e64ee..24f3a29e64ee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java +++ b/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 index 72163e4d7710..72163e4d7710 100644 --- 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 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index e2a6a5508992..e2a6a5508992 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/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 71afa62df3e0..5cc64547aa6b 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.motion.createSysUiComposeMotionTestRule 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.Scene 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 @@ class BouncerPredictiveBackTest : SysuiTestCase() { 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 a1750cdd0c84..b1ec740c5564 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 android.platform.test.annotations.EnableFlags 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.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.kosmos.testScope +import com.android.systemui.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 com.google.common.truth.Truth.assertThat 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 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { 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 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { @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 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { @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 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { @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 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { 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 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { @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 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { } @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 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() { 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 07c29a024a6c..0c65c9cbe1ef 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 com.google.common.truth.Truth.assertThat; 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 static org.mockito.Mockito.when; 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 @@ public final class MediaCoordinatorTest extends SysuiTestCase { } @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 @@ public final class MediaCoordinatorTest extends SysuiTestCase { } @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 @@ public final class MediaCoordinatorTest extends SysuiTestCase { 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 000000000000..cd681a167506 --- /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 ff8b478b368b..a80a4095a264 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 874463819c73..737aaf22b557 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 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 @@ var Kosmos.sceneKeys by Fixture { 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 @@ val Kosmos.sceneContainerViewModel by Fixture { 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 000000000000..15ed1b372db4 --- /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 000000000000..3e46c3f90b73 --- /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 6d488d21301e..60141c60a265 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.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.shade.data.repository.ShadeRepository +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -133,7 +134,7 @@ interface ShadeTestUtilDelegate { 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 @@ class ShadeTestUtilLegacyImpl( } /** 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 @@ class ShadeTestUtilSceneImpl( 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 9cdd51994262..7a15fdf95734 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 @@ package com.android.systemui.shade.ui.viewmodel 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 @@ val Kosmos.notificationsShadeOverlayContentViewModel: 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 7eb9f3472482..f5b856df8835 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 android.content.applicationContext 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 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by 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 000000000000..8fdb948e2d1d --- /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 de8b3500a88a..c3bc744e09b0 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.kosmos.testDispatcher 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 @@ val Kosmos.notificationListViewModel by Fixture { 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 3224b27d5803..73b7b35ba9a7 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ /** * 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ if (svcClientTracingEnabled()) { logTraceSvcClient("onKeyEvent", keyEvent + ", " + sequenceNumber); } - mServiceInterface.onKeyEvent(keyEvent, sequenceNumber); + mClient.onKeyEvent(keyEvent, sequenceNumber); } catch (RemoteException e) { return false; } @@ -470,7 +483,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } public boolean canReceiveEventsLocked() { - return (mEventTypes != 0 && mService != null); + return (mEventTypes != 0 && mClientBinder != null); } @RequiresNoPermission @@ -520,7 +533,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ // 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } /** - * 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ // 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ // 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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); + Slog.e( + LOG_TAG, + "Error during sending gesture " + gestureInfo + " to " + mClientBinder, + re); } } } private void notifySystemActionsChangedInternal() { - final IAccessibilityServiceClient listener = getServiceInterfaceSafely(); - if (listener != null) { + final IAccessibilityServiceClient client = getClientSafely(); + if (client != null) { try { if (svcClientTracingEnabled()) { logTraceSvcClient("onSystemActionsChanged", ""); } - listener.onSystemActionsChanged(); + client.onSystemActionsChanged(); } catch (RemoteException re) { - Slog.e(LOG_TAG, "Error sending system actions change to " + mService, - 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 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ 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 7580b697b516..d595d02016e0 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub 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 786d167af5de..15999d19ebc0 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 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect 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 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect 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 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect } private void initializeService() { - IAccessibilityServiceClient serviceInterface = null; + IAccessibilityServiceClient client = null; synchronized (mLock) { AccessibilityUserState userState = mUserStateWeakReference.get(); if (userState == null) return; @@ -272,18 +273,17 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect 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 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect 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 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect @Override public boolean isCapturingFingerprintGestures() { - return (mServiceInterface != null) + return (mClient != null) && mSecurityPolicy.canCaptureFingerprintGestures(this) && mCaptureFingerprintGestures; } @@ -506,17 +505,17 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect 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 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect 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 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect @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 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect 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 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect @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 4cb3d247edb0..cd97d838e3a0 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 @@ public class ProxyAccessibilityServiceConnection extends AccessibilityServiceCon 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 b4deeb0a6872..da11a76d5282 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 @@ public class ProxyManager { mA11yInputFilter.disableFeaturesForDisplayIfInstalled(displayId); } }); - connection.initializeServiceInterface(client); + connection.initializeClient(client); } private void registerVirtualDeviceListener() { @@ -561,8 +561,8 @@ public class ProxyManager { 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 f85d786f89c5..ed4eeb534412 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 @@ class UiAutomationManager { Binder.getCallingUserHandle().getIdentifier()); if (mUiAutomationService != null) { throw new IllegalStateException( - "UiAutomationService " + mUiAutomationService.mServiceInterface - + "already registered!"); + "UiAutomationService " + mUiAutomationService.mClient + "already registered!"); } try { @@ -130,10 +129,9 @@ class UiAutomationManager { 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 @@ class UiAutomationManager { 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 @@ class UiAutomationManager { 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 @@ class UiAutomationManager { 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 1e723b5a1da2..c8f8c2a6b223 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 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { 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 b2c679faed5d..f6ac706c4985 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * 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 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku } } + /** + * Increment the counter of widget ids and return the new id. + * + * Typically called by {@link #allocateAppWidgetId} when a instance of widget is created, + * either as a result of being pinned by launcher or added during a restore. + * + * Note: A widget id is a monotonically increasing integer that uniquely identifies the widget + * instance. + * + * TODO: Revisit this method and determine whether we need to alter the widget id during + * the restore since widget id mismatch potentially leads to some issues in the past. + */ private int incrementAndGetAppWidgetIdLocked(int userId) { final int appWidgetId = peekNextAppWidgetIdLocked(userId) + 1; mNextAppWidgetIds.put(userId, appWidgetId); return appWidgetId; } + /** + * Called by {@link #readProfileStateFromFileLocked} when widgets/providers/hosts are loaded + * from disk, which ensures mNextAppWidgetIds is larger than any existing widget id for given + * user. + */ private void setMinAppWidgetIdLocked(int userId, int minWidgetId) { final int nextAppWidgetId = peekNextAppWidgetIdLocked(userId); if (nextAppWidgetId < minWidgetId) { diff --git a/services/core/java/android/os/BatteryStatsInternal.java b/services/core/java/android/os/BatteryStatsInternal.java index 0713999d4354..60b826b50045 100644 --- a/services/core/java/android/os/BatteryStatsInternal.java +++ b/services/core/java/android/os/BatteryStatsInternal.java @@ -41,6 +41,7 @@ public abstract class BatteryStatsInternal { public static final int CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER = 3; public static final int CPU_WAKEUP_SUBSYSTEM_SENSOR = 4; public static final int CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA = 5; + public static final int CPU_WAKEUP_SUBSYSTEM_BLUETOOTH = 6; /** @hide */ @IntDef(prefix = {"CPU_WAKEUP_SUBSYSTEM_"}, value = { @@ -50,6 +51,7 @@ public abstract class BatteryStatsInternal { CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER, CPU_WAKEUP_SUBSYSTEM_SENSOR, CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA, + CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, }) @Retention(RetentionPolicy.SOURCE) public @interface CpuWakeupSubsystem { @@ -99,6 +101,14 @@ public abstract class BatteryStatsInternal { public abstract void noteCpuWakingNetworkPacket(Network network, long elapsedMillis, int uid); /** + * Informs battery stats of a sysproxy packet that woke up the CPU + * + * @param uid The uid that received the packet. + * @param elapsedMillis The time of the packet's arrival in elapsed timebase. + */ + public abstract void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis); + + /** * Informs battery stats of binder stats for the given work source UID. */ public abstract void noteBinderCallStats(int workSourceUid, long incrementalBinderCallCount, diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index 89dfc736301a..12e8c57228d6 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -380,10 +380,12 @@ public class VcnManagementService extends IVcnManagementService.Stub { } /** 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 b271d7ece733..ab69cd1ef17c 100644 --- a/services/core/java/com/android/server/WiredAccessoryManager.java +++ b/services/core/java/com/android/server/WiredAccessoryManager.java @@ -118,8 +118,11 @@ final class WiredAccessoryManager implements WiredAccessoryCallbacks { 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 @@ final class WiredAccessoryManager implements WiredAccessoryCallbacks { } @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 @@ final class WiredAccessoryManager implements WiredAccessoryCallbacks { 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 @@ final class WiredAccessoryManager implements WiredAccessoryCallbacks { * * @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 @@ final class WiredAccessoryManager implements WiredAccessoryCallbacks { 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 @@ final class WiredAccessoryManager implements WiredAccessoryCallbacks { 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 @@ final class WiredAccessoryManager implements WiredAccessoryCallbacks { 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 88edb121c0c8..3499a3a5edde 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 @@ public class AccountManagerService // 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 @@ public class AccountManagerService // 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 @@ public class AccountManagerService @Override public void onResult(Bundle result) { Bundle.setDefusable(result, true); - result = sanitizeBundle(result); IAccountManagerResponse response = getResponseAndClose(); if (response != null) { try { @@ -2462,7 +2459,6 @@ public class AccountManagerService @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 @@ public class AccountManagerService @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 @@ public class AccountManagerService @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 @@ public class AccountManagerService @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 @@ public class AccountManagerService // 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 @@ public class AccountManagerService @Override public void onResult(Bundle result) { Bundle.setDefusable(result, true); - result = sanitizeBundle(result); IAccountManagerResponse response = getResponseAndClose(); if (response == null) { return; @@ -4395,7 +4383,6 @@ public class AccountManagerService @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 @@ public class AccountManagerService 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 @@ public class AccountManagerService } } } + @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 a21a3f1a9af1..35323d6cb391 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 @@ public class ActivityManagerService extends IActivityManager.Stub 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 75e9fadbd917..ef82c7477558 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 @@ public final class BatteryStatsService extends IBatteryStats.Stub 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 final class BatteryStatsService extends IBatteryStats.Stub } public void systemServicesReady() { + mStats.saveBatteryUsageStatsOnReset(mBatteryUsageStatsProvider, mPowerStatsStore, + Flags.accumulateBatteryUsageStats()); + MultiStatePowerAttributor attributor = (MultiStatePowerAttributor) mPowerAttributor; mStats.setPowerStatsCollectorEnabled(BatteryConsumer.POWER_COMPONENT_CPU, Flags.streamlinedBatteryStats()); @@ -662,6 +664,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub } else if (nc.hasTransport(TRANSPORT_CELLULAR)) { return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; } + // For TRANSPORT_BLUETOOTH, we have a separate channel to catch Bluetooth wakeups. + // See noteCpuWakingSysproxyPacket method. return CPU_WAKEUP_SUBSYSTEM_UNKNOWN; } @@ -684,6 +688,15 @@ public final class BatteryStatsService extends IBatteryStats.Stub } @Override + public void noteCpuWakingBluetoothProxyPacket(int uid, long elapsedMillis) { + if (uid < 0) { + Slog.e(TAG, "Invalid uid for waking bluetooth proxy packet: " + uid); + return; + } + noteCpuWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, elapsedMillis, uid); + } + + @Override public void noteBinderCallStats(int workSourceUid, long incrementatCallCount, Collection<BinderCallsStats.CallStat> callStats) { synchronized (BatteryStatsService.this.mLock) { @@ -1100,14 +1113,17 @@ public final class BatteryStatsService extends IBatteryStats.Stub DEVICE_CONFIG_NAMESPACE, MIN_CONSUMED_POWER_THRESHOLD_KEY, 0); - final BatteryUsageStatsQuery query = - new BatteryUsageStatsQuery.Builder() - .setMaxStatsAgeMs(0) - .includeProcessStateData() - .includeVirtualUids() - .setMinConsumedPowerThreshold(minConsumedPowerThreshold) - .build(); - bus = getBatteryUsageStats(List.of(query)).get(0); + BatteryUsageStatsQuery.Builder query = new BatteryUsageStatsQuery.Builder() + .setMaxStatsAgeMs(0) + .includeProcessStateData() + .includeVirtualUids() + .setMinConsumedPowerThreshold(minConsumedPowerThreshold); + + if (Flags.accumulateBatteryUsageStats()) { + query.accumulated(); + } + + bus = getBatteryUsageStats(List.of(query.build())).get(0); final int pullResult = new StatsPerUidLogger(new FrameworkStatsLogger()).logStats(bus, data); try { @@ -3016,6 +3032,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub if (Flags.streamlinedBatteryStats()) { pw.println(" --sample: collect and dump a sample of stats for debugging purpose"); } + if (Flags.accumulateBatteryUsageStats()) { + pw.println(" --accumulated: continuously accumulated since setup or reset-all"); + } pw.println(" <package.name>: optional name of package to filter output by."); pw.println(" -h: print this help text."); pw.println("Battery stats (batterystats) commands:"); @@ -3083,7 +3102,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub } private void dumpUsageStats(FileDescriptor fd, PrintWriter pw, int model, - boolean proto) { + boolean proto, boolean accumulated) { awaitCompletion(); syncStats("dump", BatteryExternalStatsWorker.UPDATE_ALL); @@ -3097,6 +3116,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub if (model == BatteryConsumer.POWER_MODEL_POWER_PROFILE) { builder.powerProfileModeledOnly(); } + if (accumulated) { + builder.accumulated(); + } BatteryUsageStatsQuery query = builder.build(); synchronized (mStats) { mStats.prepareForDumpLocked(); @@ -3287,6 +3309,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub } else if ("--usage".equals(arg)) { int model = BatteryConsumer.POWER_MODEL_UNDEFINED; boolean proto = false; + boolean accumulated = false; for (int j = i + 1; j < args.length; j++) { switch (args[j]) { case "--proto": @@ -3309,9 +3332,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub } 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 3334393a1618..9b51b6ae4b0f 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 @@ flag { metadata { purpose: PURPOSE_BUGFIX } +} + +flag { + name: "logcat_longer_timeout" + namespace: "backstage_power" + description: "Wait longer during the logcat gathering operation" + bug: "292533246" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index a6389f7f5311..e0cf96fbccd0 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -1031,6 +1031,9 @@ public class AppOpsService extends IAppOpsService.Stub { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); + if (action == null) { + return; + } String pkgName = intent.getData().getEncodedSchemeSpecificPart().intern(); int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 0fd22c583192..875041540f40 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1321,9 +1321,9 @@ public class AudioDeviceBroker { sendLMsgNoDelay(MSG_II_SET_LE_AUDIO_OUT_VOLUME, SENDMSG_REPLACE, info); } - /*package*/ void postSetModeOwner(int mode, int pid, int uid) { - sendLMsgNoDelay(MSG_I_SET_MODE_OWNER, SENDMSG_REPLACE, - new AudioModeInfo(mode, pid, uid)); + /*package*/ void postSetModeOwner(int mode, int pid, int uid, boolean signal) { + sendILMsgNoDelay(MSG_IL_SET_MODE_OWNER, SENDMSG_REPLACE, + signal ? 1 : 0, new AudioModeInfo(mode, pid, uid)); } /*package*/ void postBluetoothDeviceConfigChange(@NonNull BtDeviceInfo info) { @@ -1564,38 +1564,6 @@ public class AudioDeviceBroker { sendLMsgNoDelay(MSG_L_COMMUNICATION_ROUTE_CLIENT_DIED, SENDMSG_QUEUE, client); } - /*package*/ void postSaveSetPreferredDevicesForStrategy(int strategy, - List<AudioDeviceAttributes> devices) - { - sendILMsgNoDelay(MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_QUEUE, strategy, devices); - } - - /*package*/ void postSaveRemovePreferredDevicesForStrategy(int strategy) { - sendIMsgNoDelay(MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_QUEUE, strategy); - } - - /*package*/ void postSaveSetDeviceAsNonDefaultForStrategy( - int strategy, AudioDeviceAttributes device) { - sendILMsgNoDelay(MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device); - } - - /*package*/ void postSaveRemoveDeviceAsNonDefaultForStrategy( - int strategy, AudioDeviceAttributes device) { - sendILMsgNoDelay( - MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device); - } - - /*package*/ void postSaveSetPreferredDevicesForCapturePreset( - int capturePreset, List<AudioDeviceAttributes> devices) { - sendILMsgNoDelay( - MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET, SENDMSG_QUEUE, capturePreset, devices); - } - - /*package*/ void postSaveClearPreferredDevicesForCapturePreset(int capturePreset) { - sendIMsgNoDelay( - MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET, SENDMSG_QUEUE, capturePreset); - } - /*package*/ void postUpdateCommunicationRouteClient( int btScoRequesterUid, String eventSource) { sendILMsgNoDelay(MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT, SENDMSG_QUEUE, @@ -2025,7 +1993,7 @@ public class AudioDeviceBroker { mBtHelper.setAvrcpAbsoluteVolumeIndex(msg.arg1); } break; - case MSG_I_SET_MODE_OWNER: + case MSG_IL_SET_MODE_OWNER: synchronized (mSetModeLock) { synchronized (mDeviceStateLock) { int btScoRequesterUid = bluetoothScoRequestOwnerUid(); @@ -2036,6 +2004,9 @@ public class AudioDeviceBroker { } } } + if (msg.arg1 == 1 /*signal*/) { + mAudioService.decrementAudioModeResetCount(); + } break; case MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT: @@ -2109,40 +2080,9 @@ public class AudioDeviceBroker { mDeviceInventory.setBluetoothActiveDevice(btInfo); } } break; - case MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY: { - final int strategy = msg.arg1; - final List<AudioDeviceAttributes> devices = - (List<AudioDeviceAttributes>) msg.obj; - mDeviceInventory.onSaveSetPreferredDevices(strategy, devices); - } break; - case MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY: { - final int strategy = msg.arg1; - mDeviceInventory.onSaveRemovePreferredDevices(strategy); - } break; - case MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY: { - final int strategy = msg.arg1; - final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj; - mDeviceInventory.onSaveSetDeviceAsNonDefault(strategy, device); - } break; - case MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY: { - final int strategy = msg.arg1; - final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj; - mDeviceInventory.onSaveRemoveDeviceAsNonDefault(strategy, device); - } break; case MSG_CHECK_MUTE_MUSIC: checkMessagesMuteMusic(0); break; - case MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET: { - final int capturePreset = msg.arg1; - final List<AudioDeviceAttributes> devices = - (List<AudioDeviceAttributes>) msg.obj; - mDeviceInventory.onSaveSetPreferredDevicesForCapturePreset( - capturePreset, devices); - } break; - case MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET: { - final int capturePreset = msg.arg1; - mDeviceInventory.onSaveClearPreferredDevicesForCapturePreset(capturePreset); - } break; case MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED: { final BluetoothDevice btDevice = (BluetoothDevice) msg.obj; BtHelper.onNotifyPreferredAudioProfileApplied(btDevice); @@ -2224,7 +2164,7 @@ public class AudioDeviceBroker { private static final int MSG_REPORT_NEW_ROUTES = 13; private static final int MSG_II_SET_HEARING_AID_VOLUME = 14; private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15; - private static final int MSG_I_SET_MODE_OWNER = 16; + private static final int MSG_IL_SET_MODE_OWNER = 16; private static final int MSG_I_BT_SERVICE_DISCONNECTED_PROFILE = 22; private static final int MSG_IL_BT_SERVICE_CONNECTED_PROFILE = 23; @@ -2235,16 +2175,10 @@ public class AudioDeviceBroker { // process external command to (dis)connect a hearing aid device private static final int MSG_L_HEARING_AID_DEVICE_CONNECTION_CHANGE_EXT = 31; - private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_STRATEGY = 32; - private static final int MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY = 33; - private static final int MSG_L_COMMUNICATION_ROUTE_CLIENT_DIED = 34; private static final int MSG_CHECK_MUTE_MUSIC = 35; private static final int MSG_REPORT_NEW_ROUTES_A2DP = 36; - private static final int MSG_IL_SAVE_PREF_DEVICES_FOR_CAPTURE_PRESET = 37; - private static final int MSG_I_SAVE_CLEAR_PREF_DEVICES_FOR_CAPTURE_PRESET = 38; - private static final int MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT = 42; private static final int MSG_IL_UPDATE_COMMUNICATION_ROUTE_CLIENT = 43; @@ -2253,8 +2187,6 @@ public class AudioDeviceBroker { // process set volume for Le Audio, obj is BleVolumeInfo private static final int MSG_II_SET_LE_AUDIO_OUT_VOLUME = 46; - private static final int MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY = 47; - private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48; private static final int MSG_IIL_BTLEAUDIO_TIMEOUT = 49; private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52; diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index a9bff8bf4bc3..5fd12c29d2f8 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -548,14 +548,17 @@ public class AudioDeviceInventory { @GuardedBy("mDevicesLock") private final ArrayMap<Integer, String> mApmConnectedDevices = new ArrayMap<>(); + @GuardedBy("mDevicesLock") // List of preferred devices for strategies private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevices = new ArrayMap<>(); + @GuardedBy("mDevicesLock") // List of non-default devices for strategies private final ArrayMap<Integer, List<AudioDeviceAttributes>> mNonDefaultDevices = new ArrayMap<>(); + @GuardedBy("mDevicesLock") // List of preferred devices of capture preset private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevicesForCapturePreset = new ArrayMap<>(); @@ -808,24 +811,18 @@ public class AudioDeviceInventory { synchronized (mDevicesLock) { mAppliedStrategyRoles.clear(); mAppliedPresetRoles.clear(); - } - synchronized (mPreferredDevices) { mPreferredDevices.forEach((strategy, devices) -> { setPreferredDevicesForStrategy(strategy, devices); }); - } - synchronized (mNonDefaultDevices) { mNonDefaultDevices.forEach((strategy, devices) -> { addDevicesRoleForStrategy(strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices, false /* internal */); }); - } - synchronized (mPreferredDevicesForCapturePreset) { mPreferredDevicesForCapturePreset.forEach((capturePreset, devices) -> { setDevicesRoleForCapturePreset( capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices); }); - } + } } /** only public for mocking/spying, do not call outside of AudioService */ @@ -1225,7 +1222,8 @@ public class AudioDeviceInventory { mmi.record(); } - /*package*/ void onSaveSetPreferredDevices(int strategy, + @GuardedBy("mDevicesLock") + private void saveSetPreferredDevices(int strategy, @NonNull List<AudioDeviceAttributes> devices) { mPreferredDevices.put(strategy, devices); List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy); @@ -1243,12 +1241,14 @@ public class AudioDeviceInventory { dispatchPreferredDevice(strategy, devices); } - /*package*/ void onSaveRemovePreferredDevices(int strategy) { + @GuardedBy("mDevicesLock") + private void saveRemovePreferredDevices(int strategy) { mPreferredDevices.remove(strategy); dispatchPreferredDevice(strategy, new ArrayList<AudioDeviceAttributes>()); } - /*package*/ void onSaveSetDeviceAsNonDefault(int strategy, + @GuardedBy("mDevicesLock") + private void saveSetDeviceAsNonDefault(int strategy, @NonNull AudioDeviceAttributes device) { List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy); if (nonDefaultDevices == null) { @@ -1272,7 +1272,8 @@ public class AudioDeviceInventory { } } - /*package*/ void onSaveRemoveDeviceAsNonDefault(int strategy, + @GuardedBy("mDevicesLock") + private void saveRemoveDeviceAsNonDefault(int strategy, @NonNull AudioDeviceAttributes device) { List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy); if (nonDefaultDevices != null) { @@ -1282,14 +1283,16 @@ public class AudioDeviceInventory { } } - /*package*/ void onSaveSetPreferredDevicesForCapturePreset( + @GuardedBy("mDevicesLock") + private void saveSetPreferredDevicesForCapturePreset( int capturePreset, @NonNull List<AudioDeviceAttributes> devices) { mPreferredDevicesForCapturePreset.put(capturePreset, devices); dispatchDevicesRoleForCapturePreset( capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices); } - /*package*/ void onSaveClearPreferredDevicesForCapturePreset(int capturePreset) { + @GuardedBy("mDevicesLock") + private void saveClearPreferredDevicesForCapturePreset(int capturePreset) { mPreferredDevicesForCapturePreset.remove(capturePreset); dispatchDevicesRoleForCapturePreset( capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, @@ -1301,21 +1304,22 @@ public class AudioDeviceInventory { /*package*/ int setPreferredDevicesForStrategyAndSave(int strategy, @NonNull List<AudioDeviceAttributes> devices) { - final int status = setPreferredDevicesForStrategy(strategy, devices); - if (status == AudioSystem.SUCCESS) { - mDeviceBroker.postSaveSetPreferredDevicesForStrategy(strategy, devices); + synchronized(mDevicesLock){ + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + final int status = setPreferredDevicesForStrategy(strategy, devices); + if (status == AudioSystem.SUCCESS) { + saveSetPreferredDevices(strategy, devices); + } + return status; + } } - return status; } // Only used for external requests coming from an API /*package*/ int setPreferredDevicesForStrategy(int strategy, @NonNull List<AudioDeviceAttributes> devices) { - int status = AudioSystem.ERROR; - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - status = setDevicesRoleForStrategy( - strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices, false /* internal */); - } - return status; + + return setDevicesRoleForStrategy( + strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices, false /* internal */); } // Only used for internal requests /*package*/ int setPreferredDevicesForStrategyInt(int strategy, @@ -1326,21 +1330,21 @@ public class AudioDeviceInventory { } /*package*/ int removePreferredDevicesForStrategyAndSave(int strategy) { - final int status = removePreferredDevicesForStrategy(strategy); - if (status == AudioSystem.SUCCESS) { - mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy); + synchronized(mDevicesLock){ + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + final int status = removePreferredDevicesForStrategy(strategy); + if (status == AudioSystem.SUCCESS) { + saveRemovePreferredDevices(strategy); + } + return status; + } } - return status; } // Only used for external requests coming from an API /*package*/ int removePreferredDevicesForStrategy(int strategy) { - int status = AudioSystem.ERROR; - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - status = clearDevicesRoleForStrategy( + return clearDevicesRoleForStrategy( strategy, AudioSystem.DEVICE_ROLE_PREFERRED, false /*internal */); - } - return status; } // Only used for internal requests /*package*/ int removePreferredDevicesForStrategyInt(int strategy) { @@ -1351,16 +1355,17 @@ public class AudioDeviceInventory { /*package*/ int setDeviceAsNonDefaultForStrategyAndSave(int strategy, @NonNull AudioDeviceAttributes device) { int status = AudioSystem.ERROR; - - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - List<AudioDeviceAttributes> devices = new ArrayList<>(); - devices.add(device); - status = addDevicesRoleForStrategy( + synchronized(mDevicesLock){ + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + List<AudioDeviceAttributes> devices = new ArrayList<>(); + devices.add(device); + status = addDevicesRoleForStrategy( strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices, false /* internal */); - } - if (status == AudioSystem.SUCCESS) { - mDeviceBroker.postSaveSetDeviceAsNonDefaultForStrategy(strategy, device); + if (status == AudioSystem.SUCCESS) { + saveSetDeviceAsNonDefault(strategy, device); + } + } } return status; } @@ -1368,16 +1373,17 @@ public class AudioDeviceInventory { /*package*/ int removeDeviceAsNonDefaultForStrategyAndSave(int strategy, @NonNull AudioDeviceAttributes device) { int status = AudioSystem.ERROR; - - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - List<AudioDeviceAttributes> devices = new ArrayList<>(); - devices.add(device); - status = removeDevicesRoleForStrategy( + synchronized(mDevicesLock){ + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + List<AudioDeviceAttributes> devices = new ArrayList<>(); + devices.add(device); + status = removeDevicesRoleForStrategy( strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices, false /* internal */); - } - if (status == AudioSystem.SUCCESS) { - mDeviceBroker.postSaveRemoveDeviceAsNonDefaultForStrategy(strategy, device); + if (status == AudioSystem.SUCCESS) { + saveRemoveDeviceAsNonDefault(strategy, device); + } + } } return status; } @@ -1405,41 +1411,40 @@ public class AudioDeviceInventory { /*package*/ int setPreferredDevicesForCapturePresetAndSave( int capturePreset, @NonNull List<AudioDeviceAttributes> devices) { - final int status = setPreferredDevicesForCapturePreset(capturePreset, devices); - if (status == AudioSystem.SUCCESS) { - mDeviceBroker.postSaveSetPreferredDevicesForCapturePreset(capturePreset, devices); + synchronized(mDevicesLock){ + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + final int status = setPreferredDevicesForCapturePreset(capturePreset, devices); + if (status == AudioSystem.SUCCESS) { + saveSetPreferredDevicesForCapturePreset(capturePreset, devices); + } + return status; + } } - return status; } // Only used for external requests coming from an API private int setPreferredDevicesForCapturePreset( int capturePreset, @NonNull List<AudioDeviceAttributes> devices) { - int status = AudioSystem.ERROR; - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - status = setDevicesRoleForCapturePreset( + return setDevicesRoleForCapturePreset( capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices); - } - return status; } /*package*/ int clearPreferredDevicesForCapturePresetAndSave(int capturePreset) { - final int status = clearPreferredDevicesForCapturePreset(capturePreset); - if (status == AudioSystem.SUCCESS) { - mDeviceBroker.postSaveClearPreferredDevicesForCapturePreset(capturePreset); + synchronized(mDevicesLock){ + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + final int status = clearPreferredDevicesForCapturePreset(capturePreset); + if (status == AudioSystem.SUCCESS) { + saveClearPreferredDevicesForCapturePreset(capturePreset); + } + return status; + } } - return status; } // Only used for external requests coming from an API private int clearPreferredDevicesForCapturePreset(int capturePreset) { - int status = AudioSystem.ERROR; - - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - status = clearDevicesRoleForCapturePreset( + return clearDevicesRoleForCapturePreset( capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED); - } - return status; } // Only used for internal requests diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index e83b03690a24..561030e77e61 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -1915,7 +1915,7 @@ public class AudioService extends IAudioService.Stub // Restore call state synchronized (mDeviceBroker.mSetModeLock) { onUpdateAudioMode(AudioSystem.MODE_CURRENT, android.os.Process.myPid(), - mContext.getPackageName(), true /*force*/); + mContext.getPackageName(), true /*force*/, false /*signal*/); } final int forSys; synchronized (mSettingsLock) { @@ -4743,14 +4743,47 @@ public class AudioService extends IAudioService.Stub } } if (updateAudioMode) { - sendMsg(mAudioHandler, - MSG_UPDATE_AUDIO_MODE, - existingMsgPolicy, - AudioSystem.MODE_CURRENT, - android.os.Process.myPid(), - mContext.getPackageName(), - delay); + postUpdateAudioMode(existingMsgPolicy, AudioSystem.MODE_CURRENT, + android.os.Process.myPid(), mContext.getPackageName(), + false /*signal*/, delay); + } + } + } + + static class UpdateAudioModeInfo { + UpdateAudioModeInfo(int mode, int pid, String packageName, boolean signal) { + mMode = mode; + mPid = pid; + mPackageName = packageName; + mSignal = signal; + } + private final int mMode; + private final int mPid; + private final String mPackageName; + private final boolean mSignal; + + int getMode() { + return mMode; + } + int getPid() { + return mPid; + } + String getPackageName() { + return mPackageName; + } + boolean getSignal() { + return mSignal; + } + } + + void postUpdateAudioMode(int msgPolicy, int mode, int pid, String packageName, + boolean signal, int delay) { + synchronized (mAudioModeResetLock) { + if (signal) { + mAudioModeResetCount++; } + sendMsg(mAudioHandler, MSG_UPDATE_AUDIO_MODE, msgPolicy, 0, 0, + new UpdateAudioModeInfo(mode, pid, packageName, signal), delay); } } @@ -6152,13 +6185,9 @@ public class AudioService extends IAudioService.Stub } else { SetModeDeathHandler h = mSetModeDeathHandlers.get(index); mSetModeDeathHandlers.remove(index); - sendMsg(mAudioHandler, - MSG_UPDATE_AUDIO_MODE, - SENDMSG_QUEUE, - AudioSystem.MODE_CURRENT, - android.os.Process.myPid(), - mContext.getPackageName(), - 0); + postUpdateAudioMode(SENDMSG_QUEUE, AudioSystem.MODE_CURRENT, + android.os.Process.myPid(), mContext.getPackageName(), + false /*signal*/, 0); } } } @@ -6404,19 +6433,14 @@ public class AudioService extends IAudioService.Stub } } - sendMsg(mAudioHandler, - MSG_UPDATE_AUDIO_MODE, - SENDMSG_REPLACE, - mode, - pid, - callingPackage, - 0); + postUpdateAudioMode(SENDMSG_REPLACE, mode, pid, callingPackage, + hasModifyPhoneStatePermission && mode == AudioSystem.MODE_NORMAL, 0); } } @GuardedBy("mDeviceBroker.mSetModeLock") void onUpdateAudioMode(int requestedMode, int requesterPid, String requesterPackage, - boolean force) { + boolean force, boolean signal) { if (requestedMode == AudioSystem.MODE_CURRENT) { requestedMode = getMode(); } @@ -6431,7 +6455,7 @@ public class AudioService extends IAudioService.Stub } if (DEBUG_MODE) { Log.v(TAG, "onUpdateAudioMode() new mode: " + mode + ", current mode: " - + mMode.get() + " requested mode: " + requestedMode); + + mMode.get() + " requested mode: " + requestedMode + " signal: " + signal); } if (mode != mMode.get() || force) { int status = AudioSystem.SUCCESS; @@ -6477,7 +6501,7 @@ public class AudioService extends IAudioService.Stub // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO // connections not started by the application changing the mode when pid changes - mDeviceBroker.postSetModeOwner(mode, pid, uid); + mDeviceBroker.postSetModeOwner(mode, pid, uid, signal); } else { Log.w(TAG, "onUpdateAudioMode: failed to set audio mode to: " + mode); } @@ -10162,7 +10186,7 @@ public class AudioService extends IAudioService.Stub h.setRecordingActive(isRecordingActiveForUid(h.getUid())); if (wasActive != h.isActive()) { onUpdateAudioMode(AudioSystem.MODE_CURRENT, android.os.Process.myPid(), - mContext.getPackageName(), false /*force*/); + mContext.getPackageName(), false /*force*/, false /*signal*/); } } break; @@ -10192,7 +10216,9 @@ public class AudioService extends IAudioService.Stub case MSG_UPDATE_AUDIO_MODE: synchronized (mDeviceBroker.mSetModeLock) { - onUpdateAudioMode(msg.arg1, msg.arg2, (String) msg.obj, false /*force*/); + UpdateAudioModeInfo info = (UpdateAudioModeInfo) msg.obj; + onUpdateAudioMode(info.getMode(), info.getPid(), info.getPackageName(), + false /*force*/, info.getSignal()); } break; @@ -10895,9 +10921,59 @@ public class AudioService extends IAudioService.Stub return AudioManager.AUDIOFOCUS_REQUEST_FAILED; } mmi.record(); + //delay abandon focus requests from Telecom if an audio mode reset from Telecom + // is still being processed + final boolean abandonFromTelecom = (mContext.checkCallingOrSelfPermission( + MODIFY_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) + && ((aa != null && aa.getUsage() == AudioAttributes.USAGE_VOICE_COMMUNICATION) + || AudioSystem.IN_VOICE_COMM_FOCUS_ID.equals(clientId)); + if (abandonFromTelecom) { + synchronized (mAudioModeResetLock) { + final long start = java.lang.System.currentTimeMillis(); + long elapsed = 0; + while (mAudioModeResetCount > 0) { + if (DEBUG_MODE) { + Log.i(TAG, "Abandon focus from Telecom, waiting for mode change"); + } + try { + mAudioModeResetLock.wait( + AUDIO_MODE_RESET_TIMEOUT_MS - elapsed); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted while waiting for audio mode reset"); + } + elapsed = java.lang.System.currentTimeMillis() - start; + if (elapsed >= AUDIO_MODE_RESET_TIMEOUT_MS) { + Log.e(TAG, "Timeout waiting for audio mode reset"); + break; + } + } + if (DEBUG_MODE && elapsed != 0) { + Log.i(TAG, "Abandon focus from Telecom done waiting"); + } + } + } return mMediaFocusControl.abandonAudioFocus(fd, clientId, aa, callingPackageName); } + /** synchronization between setMode(NORMAL) and abandonAudioFocus() frmo Telecom */ + private static final long AUDIO_MODE_RESET_TIMEOUT_MS = 3000; + + private final Object mAudioModeResetLock = new Object(); + + @GuardedBy("mAudioModeResetLock") + private int mAudioModeResetCount = 0; + + void decrementAudioModeResetCount() { + synchronized (mAudioModeResetLock) { + if (mAudioModeResetCount > 0) { + mAudioModeResetCount--; + } else { + Log.w(TAG, "mAudioModeResetCount already 0"); + } + mAudioModeResetLock.notify(); + } + } + /** see {@link AudioManager#abandonAudioFocusForTest(AudioFocusRequest, String)} */ public int abandonAudioFocusForTest(IAudioFocusDispatcher fd, String clientId, AudioAttributes aa, String callingPackageName) { diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index ce92dfbcc1c8..b4212641346f 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 @@ public class BtHelper { 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 @@ public class BtHelper { 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 dc263c5a6b32..af9c9accb635 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 class DisplayDeviceConfig { */ public float[] getNits() { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mNits; + return mEvenDimmerBrightnessData.nits; } return mNits; } @@ -1093,7 +1093,7 @@ public class DisplayDeviceConfig { @VisibleForTesting public float[] getBacklight() { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mBacklight; + return mEvenDimmerBrightnessData.backlight; } return mBacklight; } @@ -1107,7 +1107,7 @@ public class DisplayDeviceConfig { */ 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 class DisplayDeviceConfig { */ 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 @@ public class DisplayDeviceConfig { */ private Spline getBacklightToBrightnessSpline() { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mBacklightToBrightness; + return mEvenDimmerBrightnessData.backlightToBrightness; } return mBacklightToBrightnessSpline; } @@ -1144,11 +1144,11 @@ public class DisplayDeviceConfig { */ 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 class DisplayDeviceConfig { */ 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 @@ public class DisplayDeviceConfig { if (mEvenDimmerBrightnessData == null) { return INVALID_NITS; } - return mEvenDimmerBrightnessData.mMinLuxToNits.interpolate(lux); + return mEvenDimmerBrightnessData.minLuxToNits.interpolate(lux); } /** @@ -1197,7 +1197,7 @@ public class DisplayDeviceConfig { if (mEvenDimmerBrightnessData == null) { return PowerManager.BRIGHTNESS_MIN; } - return mEvenDimmerBrightnessData.mTransitionPoint; + return mEvenDimmerBrightnessData.transitionPoint; } /** @@ -1268,7 +1268,7 @@ public class DisplayDeviceConfig { */ public float[] getBrightness() { if (mEvenDimmerBrightnessData != null) { - return mEvenDimmerBrightnessData.mBrightness; + return mEvenDimmerBrightnessData.brightness; } return mBrightness; } @@ -2617,13 +2617,13 @@ public class DisplayDeviceConfig { 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 @@ public class DisplayDeviceConfig { + 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 ae33b83b49dc..88907e35854f 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 @@ public final class DisplayManagerService extends SystemService { } @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 711bc7972091..62d876102720 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 android.hardware.display.DisplayManagerInternal.DisplayPowerReques 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 class DisplayPowerController implements AutomaticBrightnessController.Call 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 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call 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 0570b2ab510b..5edea0a8b031 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.SystemProperties; 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 @@ final class LocalDisplayAdapter extends DisplayAdapter { 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 @@ final class LocalDisplayAdapter extends DisplayAdapter { 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 @@ final class LocalDisplayAdapter extends DisplayAdapter { 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 @@ final class LocalDisplayAdapter extends DisplayAdapter { } 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 @@ final class LocalDisplayAdapter extends DisplayAdapter { 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 @@ final class LocalDisplayAdapter extends DisplayAdapter { 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 ad57ebfb0600..9e9b899ffa7d 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 final class BrightnessEvent { 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 @@ public final class BrightnessEvent { + ((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 bd759f378d5b..dc59e66d85f2 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.provider.Settings.System; 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 @@ public final class ColorDisplayService extends SystemService { 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 @@ public final class ColorDisplayService extends SystemService { 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 @@ public final class ColorDisplayService extends SystemService { } /** + * 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 @@ public final class ColorDisplayService extends SystemService { } /** + * + * @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 @@ public final class ColorDisplayService extends SystemService { * 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 f529c4c65a9a..4f6fbd003d9a 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 @@ public class ReduceBrightColorsTintController extends TintController { 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 7e2e10a7639f..9a590a293ea3 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 @@ public class EvenDimmerBrightnessData { /** * 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 @@ public class EvenDimmerBrightnessData { if (map == null) { return null; } - String interpolation = map.getInterpolation(); List<BrightnessPoint> brightnessPoints = map.getBrightnessPoint(); if (brightnessPoints.isEmpty()) { @@ -169,22 +172,11 @@ public class EvenDimmerBrightnessData { ++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 92812670057a..99f7f12567b4 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.graphics.PointF; 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 @@ public abstract class InputManagerInternal { /** * 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 fd7479eb8d48..f04557665477 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 android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M 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.os.vibrator.VibrationEffectSegment; 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.SurfaceControl; 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 @@ public class InputManagerService extends IInputManager.Stub // 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 @@ public class InputManagerService extends IInputManager.Stub mKeyRemapper.systemRunning(); mPointerIconCache.systemRunning(); mKeyboardGlyphManager.systemRunning(); + mKeyGestureController.systemRunning(); initKeyGestures(); } @@ -2469,6 +2476,14 @@ public class InputManagerService extends IInputManager.Stub 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 @@ public class InputManagerService extends IInputManager.Stub 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 @@ public class InputManagerService extends IInputManager.Stub // 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 @@ public class InputManagerService extends IInputManager.Stub 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 @@ public class InputManagerService extends IInputManager.Stub } @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 4538b49b73c5..b488db533d12 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.Looper; 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { || 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 @@ final class KeyGestureController { // 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { @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 @@ final class KeyGestureController { } } + 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 @@ final class KeyGestureController { 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 @@ final class KeyGestureController { 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 d17e256e34fc..4404d63e02fc 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 @@ interface NativeInputManagerService { void setShowTouches(boolean enabled); - void setInteractive(boolean interactive); + void setNonInteractiveDisplays(int[] displayIds); void reloadCalibration(); @@ -409,7 +409,7 @@ interface NativeInputManagerService { 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 a1e5ebc002a5..cf0c5b094c4d 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 @@ public class TouchpadDebugView extends LinearLayout { @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 2bb2b7b21cce..f0fb33eaaee7 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 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. 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 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. 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 3023603dc437..84456327526a 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 @@ package com.android.server.inputmethod; 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 class SoftInputShowHideHistory { final String mImeTargetNameFromWm; @Nullable final String mImeSurfaceParentName; + @UserIdInt + final int mImeUserId; Entry(ClientState client, EditorInfo editorInfo, String focusedWindowName, @@ -69,7 +72,7 @@ final class SoftInputShowHideHistory { @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 @@ final class SoftInputShowHideHistory { mImeControlTargetName = imeControlTargetName; mImeTargetNameFromWm = imeTargetName; mImeSurfaceParentName = imeSurfaceParentName; + mImeUserId = imeUserId; } } @@ -102,7 +106,8 @@ final class SoftInputShowHideHistory { 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 dc62b27dec05..c3a714b0eef0 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 @@ public class NotificationManagerService extends SystemService { } @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 @@ public class NotificationManagerService extends SystemService { 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 @@ public class NotificationManagerService extends SystemService { } @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 fcc8d2f74ce9..85c395781d0a 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_P 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 @@ public class PreferencesHelper implements RankingConfig { /** * 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 @@ public class PreferencesHelper implements RankingConfig { } } - @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 @@ public class PreferencesHelper implements RankingConfig { 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 f6d9dc29d330..03a34f20c311 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.infra.ServiceConnector; 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 @@ public class OnDeviceIntelligenceManagerService extends SystemService { 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 8410cff74265..fe9a85989cc9 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 @@ public class PersistentDataBlockService extends SystemService { 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 @@ public class PersistentDataBlockService extends SystemService { enforceChecksumValidity(); if (mFrpEnforced) { automaticallyDeactivateFrpIfPossible(); - setOemUnlockEnabledProperty(doGetOemUnlockEnabled()); setOldSettingForBackworkCompatibility(mFrpActive); } else { formatIfOemUnlockEnabled(); @@ -303,10 +301,6 @@ public class PersistentDataBlockService extends SystemService { } } - 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 @@ public class PersistentDataBlockService extends SystemService { formatPartitionLocked(true); } } - setOemUnlockEnabledProperty(enabled); } private void enforceOemUnlockReadPermission() { @@ -814,17 +807,9 @@ public class PersistentDataBlockService extends SystemService { 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 4665a72b0b06..89ced12e873d 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 @@ public class ComputerEngine implements Computer { 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 5653da07779b..8657de24d725 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 @@ public class LauncherAppsService extends SystemService { 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 1316df16027f..b1b1637c890b 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.PendingIntent; 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 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements 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 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements .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 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } } + 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 ff9c3e5f467b..611e0d86202a 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 @@ public class PackageManagerService implements PackageSender, TestUtilityService 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 df9f7fb3d6e5..5fc3e332b95c 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 @@ public class PermissionManagerService extends IPermissionManager.Stub { 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 9dfaca8d163f..1592ef306cb3 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 @@ public class KeyCombinationManager { * }; * </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 @@ public class KeyCombinationManager { } // 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 @@ public class KeyCombinationManager { } } - 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 @@ public class KeyCombinationManager { * 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 @@ public class KeyCombinationManager { /** * 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 @@ public class KeyCombinationManager { /** * 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 @@ public class KeyCombinationManager { /** * 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 @@ public class KeyCombinationManager { 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 ca6051874d78..228405074dbb 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 android.view.contentprotection.flags.Flags.createAccessibilityOver 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.HapticFeedbackConstants; 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { // 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { // 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } @Override - void execute() { + public void execute() { switch (mPowerVolUpBehavior) { case POWER_VOLUME_UP_BEHAVIOR_MUTE: // no haptic feedback here since @@ -2551,7 +2552,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } @Override - void cancel() { + public void cancel() { switch (mPowerVolUpBehavior) { case POWER_VOLUME_UP_BEHAVIOR_MUTE: cancelPendingRingerToggleChordAction(); @@ -2567,16 +2568,16 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { + 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { // 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { @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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { mHandler.removeMessages(MSG_ACCESSIBILITY_TV); } - private void requestBugreportForTv() { + @VisibleForTesting + void requestBugreportForTv() { try { if (!ActivityManager.getService().launchBugReportHandlerApp()) { ActivityManager.getService().requestInteractiveBugReport(); @@ -4259,7 +4404,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { // 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { + ", keyCode=" + event.getKeyCode() + ", scanCode=" + event.getScanCode() + ", metaState=" + event.getMetaState() - + ", repeatCount=" + event.getRepeatCount() - + ", policyFlags=" + policyFlags); - } - - if (interceptUnhandledKey(event, focusedToken)) { - return null; + + ", repeatCount=" + event.getRepeatCount()); } - 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } break; - case KeyEvent.KEYCODE_STEM_PRIMARY: - handleUnhandledSystemKey(event); - sendSystemKeyToStatusBarAsync(event); - return true; } return false; @@ -4419,19 +4511,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 @@ public class PhoneWindowManager implements WindowManagerPolicy { 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 892af6bec534..ad116578ae41 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 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants { * @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 303828f94e8a..0cdf537b3455 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.telephony.TelephonyManager; 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 @@ public class Notifier { } // 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 c6ef89dcff69..fd60e06c0762 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 @@ public class PowerManagerFlags { 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 @@ public class PowerManagerFlags { } /** + * @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 @@ public class PowerManagerFlags { 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 3581b2fad1df..9cf3bb6df3db 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 @@ flag { 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 000000000000..dd6d5dbf753c --- /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 cb8e1a0f35b8..3f1d9a3b87a0 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 @@ public class BatteryStatsImpl extends BatteryStats { } private boolean mSaveBatteryUsageStatsOnReset; + private boolean mAccumulateBatteryUsageStats; private BatteryUsageStatsProvider mBatteryUsageStatsProvider; private PowerStatsStore mPowerStatsStore; @@ -11975,10 +11976,12 @@ public class BatteryStatsImpl extends BatteryStats { */ 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 @@ public class BatteryStatsImpl extends BatteryStats { 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 87a3e5e14e0d..d66e05bd6f2a 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.Clock; 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 @@ public class BatteryUsageStatsProvider { mClock = clock; mPowerStatsStore.addSectionReader(new BatteryUsageStatsSection.Reader()); + mPowerStatsStore.addSectionReader(new AccumulatedBatteryUsageStatsSection.Reader()); } private List<PowerCalculator> getPowerCalculators() { @@ -151,6 +153,56 @@ public class BatteryUsageStatsProvider { } /** + * 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 @@ public class BatteryUsageStatsProvider { 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 @@ public class BatteryUsageStatsProvider { 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 a875c30c9ebc..5a6f973424d1 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 @@ public class PowerStatsStore { } /** + * 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 @@ public class PowerStatsStore { 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 05d29f50085c..ce6f57fec0a7 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 @@ flag { bug: "364350206" is_fixed_read_only: true } + +flag { + name: "accumulate_battery_usage_stats" + namespace: "backstage_power" + description: "Add support for monotonically accumulated BatteryUsageStats" + bug: "345022340" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java index f047f564538d..ab630eef4644 100644 --- a/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java +++ b/services/core/java/com/android/server/power/stats/wakeups/CpuWakeupStats.java @@ -17,6 +17,7 @@ package com.android.server.power.stats.wakeups; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM; +import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER; @@ -63,6 +64,7 @@ public class CpuWakeupStats { private static final String SUBSYSTEM_SOUND_TRIGGER_STRING = "Sound_trigger"; private static final String SUBSYSTEM_SENSOR_STRING = "Sensor"; private static final String SUBSYSTEM_CELLULAR_DATA_STRING = "Cellular_data"; + private static final String SUBSYSTEM_BLUETOOTH_STRING = "Bluetooth"; private static final String TRACE_TRACK_WAKEUP_ATTRIBUTION = "wakeup_attribution"; private static final long WAKEUP_WRITE_DELAY_MS = TimeUnit.SECONDS.toMillis(30); @@ -512,6 +514,8 @@ public class CpuWakeupStats { return CPU_WAKEUP_SUBSYSTEM_SENSOR; case SUBSYSTEM_CELLULAR_DATA_STRING: return CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; + case SUBSYSTEM_BLUETOOTH_STRING: + return CPU_WAKEUP_SUBSYSTEM_BLUETOOTH; } return CPU_WAKEUP_SUBSYSTEM_UNKNOWN; } @@ -528,6 +532,8 @@ public class CpuWakeupStats { return SUBSYSTEM_SENSOR_STRING; case CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA: return SUBSYSTEM_CELLULAR_DATA_STRING; + case CPU_WAKEUP_SUBSYSTEM_BLUETOOTH: + return SUBSYSTEM_BLUETOOTH_STRING; case CPU_WAKEUP_SUBSYSTEM_UNKNOWN: return "Unknown"; } diff --git a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java index b4d3862431f1..b5a7fcb72982 100644 --- a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java +++ b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java @@ -16,6 +16,7 @@ package com.android.server.vibrator; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.os.ExternalVibration; @@ -24,6 +25,7 @@ import android.os.IBinder; import android.os.VibrationAttributes; import android.os.vibrator.Flags; +import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FrameworkStatsLog; /** @@ -32,14 +34,16 @@ import com.android.internal.util.FrameworkStatsLog; final class ExternalVibrationSession extends Vibration implements VibrationSession, IBinder.DeathRecipient { + private final Object mLock = new Object(); private final ExternalVibration mExternalVibration; private final ExternalVibrationScale mScale = new ExternalVibrationScale(); + @GuardedBy("mLock") @Nullable private Runnable mBinderDeathCallback; ExternalVibrationSession(ExternalVibration externalVibration) { - super(externalVibration.getToken(), new CallerInfo( + super(new CallerInfo( externalVibration.getVibrationAttributes(), externalVibration.getUid(), // TODO(b/249785241): Find a way to link ExternalVibration to a VirtualDevice // instead of using DEVICE_ID_INVALID here and relying on the UID checks. @@ -82,24 +86,25 @@ final class ExternalVibrationSession extends Vibration } @Override - public void linkToDeath(Runnable callback) { - synchronized (this) { + public boolean linkToDeath(Runnable callback) { + synchronized (mLock) { mBinderDeathCallback = callback; } mExternalVibration.linkToDeath(this); + return true; } @Override public void unlinkToDeath() { mExternalVibration.unlinkToDeath(this); - synchronized (this) { + synchronized (mLock) { mBinderDeathCallback = null; } } @Override public void binderDied() { - synchronized (this) { + synchronized (mLock) { if (mBinderDeathCallback != null) { mBinderDeathCallback.run(); } @@ -119,9 +124,11 @@ final class ExternalVibrationSession extends Vibration } @Override - public void notifyEnded() { + public void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy, + boolean immediate) { // Notify external client that this vibration should stop sending data to the vibrator. mExternalVibration.mute(); + end(new EndInfo(status, endedBy)); } boolean isHoldingSameVibration(ExternalVibration vib) { diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java index ea4bd0182ea3..ce9c47ba6ba4 100644 --- a/services/core/java/com/android/server/vibrator/HalVibration.java +++ b/services/core/java/com/android/server/vibrator/HalVibration.java @@ -36,6 +36,7 @@ import java.util.concurrent.CountDownLatch; final class HalVibration extends Vibration { public final SparseArray<VibrationEffect> mFallbacks = new SparseArray<>(); + public final IBinder callerToken; /** A {@link CountDownLatch} to enable waiting for completion. */ private final CountDownLatch mCompletionLatch = new CountDownLatch(1); @@ -55,9 +56,10 @@ final class HalVibration extends Vibration { private int mScaleLevel; private float mAdaptiveScale; - HalVibration(@NonNull IBinder token, @NonNull CombinedVibration effect, + HalVibration(@NonNull IBinder callerToken, @NonNull CombinedVibration effect, @NonNull VibrationSession.CallerInfo callerInfo) { - super(token, callerInfo); + super(callerInfo); + this.callerToken = callerToken; mOriginalEffect = effect; mEffectToPlay = effect; mScaleLevel = VibrationScaler.SCALE_NONE; diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java index 9a0479337471..21fd4ce0acd0 100644 --- a/services/core/java/com/android/server/vibrator/Vibration.java +++ b/services/core/java/com/android/server/vibrator/Vibration.java @@ -53,16 +53,13 @@ abstract class Vibration { public final long id; public final VibrationSession.CallerInfo callerInfo; public final VibrationStats stats = new VibrationStats(); - public final IBinder callerToken; private VibrationSession.Status mStatus; - Vibration(@NonNull IBinder token, @NonNull VibrationSession.CallerInfo callerInfo) { - Objects.requireNonNull(token); + Vibration(@NonNull VibrationSession.CallerInfo callerInfo) { Objects.requireNonNull(callerInfo); mStatus = VibrationSession.Status.RUNNING; this.id = sNextVibrationId.getAndIncrement(); - this.callerToken = token; this.callerInfo = callerInfo; } diff --git a/services/core/java/com/android/server/vibrator/VibrationSession.java b/services/core/java/com/android/server/vibrator/VibrationSession.java index 5640b49b28d8..70477a26b759 100644 --- a/services/core/java/com/android/server/vibrator/VibrationSession.java +++ b/services/core/java/com/android/server/vibrator/VibrationSession.java @@ -49,14 +49,26 @@ interface VibrationSession { * Links this session to the app process death with given callback to handle it. * * <p>This can be used by the service to end the vibration session when the app process dies. + * + * @param callback The service callback to be triggered when the binder dies + * @return true if the link was successful, false otherwise */ - void linkToDeath(Runnable callback); + boolean linkToDeath(@Nullable Runnable callback); /** Removes link to the app process death. */ void unlinkToDeath(); - /** Notify the session end was requested, which might be acted upon asynchronously. */ - void notifyEnded(); + /** + * Notify the session end was requested, which might be acted upon asynchronously. + * + * <p>Only the first end signal will be used to end a session, but subsequent calls with + * {@code immediate} flag set to true can still force it to take effect urgently. + * + * @param status the end status. + * @param endedBy the {@link CallerInfo} of the session that requested this session to end. + * @param immediate indicates whether cancellation should abort urgently and skip cleanup steps. + */ + void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy, boolean immediate); /** * Session status with reference to values from vibratormanagerservice.proto for logging. @@ -119,7 +131,7 @@ interface VibrationSession { public final String reason; CallerInfo(@NonNull VibrationAttributes attrs, int uid, int deviceId, String opPkg, - String reason) { + @Nullable String reason) { Objects.requireNonNull(attrs); this.attrs = attrs; this.uid = uid; diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java index 69cdcf47680d..9cb8c1ada246 100644 --- a/services/core/java/com/android/server/vibrator/VibrationSettings.java +++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java @@ -31,6 +31,7 @@ import static android.os.VibrationAttributes.USAGE_UNKNOWN; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.IActivityManager; import android.app.SynchronousUserSwitchObserver; import android.app.UidObserver; import android.content.BroadcastReceiver; @@ -74,6 +75,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; /** Controls all the system settings related to vibration. */ @@ -147,9 +149,6 @@ final class VibrationSettings { PowerManager.GO_TO_SLEEP_REASON_INATTENTIVE, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT)); - private static final IntentFilter INTERNAL_RINGER_MODE_CHANGED_INTENT_FILTER = - new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); - /** Listener for changes on vibration settings. */ interface OnVibratorSettingsChanged { /** Callback triggered when any of the vibrator settings change. */ @@ -158,15 +157,18 @@ final class VibrationSettings { private final Object mLock = new Object(); private final Context mContext; - private final String mSystemUiPackage; @VisibleForTesting final SettingsContentObserver mSettingObserver; @VisibleForTesting - final SettingsBroadcastReceiver mSettingChangeReceiver; + final RingerModeBroadcastReceiver mRingerModeBroadcastReceiver; + @VisibleForTesting + final BatteryBroadcastReceiver mBatteryBroadcastReceiver; @VisibleForTesting final VibrationUidObserver mUidObserver; @VisibleForTesting final VibrationUserSwitchObserver mUserSwitchObserver; + @VisibleForTesting + final VibrationLowPowerModeListener mLowPowerModeListener; @GuardedBy("mLock") private final List<OnVibratorSettingsChanged> mListeners = new ArrayList<>(); @@ -180,10 +182,13 @@ final class VibrationSettings { @GuardedBy("mLock") @Nullable private PowerManagerInternal mPowerManagerInternal; + @GuardedBy("mLock") @Nullable private VirtualDeviceManagerInternal mVirtualDeviceManagerInternal; @GuardedBy("mLock") + private String mSystemUiPackage; + @GuardedBy("mLock") private boolean mVibrateInputDevices; @GuardedBy("mLock") private SparseIntArray mCurrentVibrationIntensities = new SparseIntArray(); @@ -205,11 +210,11 @@ final class VibrationSettings { mContext = context; mVibrationConfig = config; mSettingObserver = new SettingsContentObserver(handler); - mSettingChangeReceiver = new SettingsBroadcastReceiver(); + mRingerModeBroadcastReceiver = new RingerModeBroadcastReceiver(); + mBatteryBroadcastReceiver = new BatteryBroadcastReceiver(); mUidObserver = new VibrationUidObserver(); mUserSwitchObserver = new VibrationUserSwitchObserver(); - mSystemUiPackage = LocalServices.getService(PackageManagerInternal.class) - .getSystemUiServiceComponent().getPackageName(); + mLowPowerModeListener = new VibrationLowPowerModeListener(); VibrationEffect clickEffect = createEffectFromResource( com.android.internal.R.array.config_virtualKeyVibePattern); @@ -233,18 +238,34 @@ final class VibrationSettings { } public void onSystemReady() { - PowerManagerInternal pm = LocalServices.getService(PowerManagerInternal.class); - AudioManager am = mContext.getSystemService(AudioManager.class); - int ringerMode = am.getRingerModeInternal(); + onSystemReady(LocalServices.getService(PackageManagerInternal.class), + LocalServices.getService(PowerManagerInternal.class), + ActivityManager.getService(), + LocalServices.getService(VirtualDeviceManagerInternal.class), + mContext.getSystemService(AudioManager.class)); + } + + @VisibleForTesting + void onSystemReady(PackageManagerInternal packageManagerInternal, + PowerManagerInternal powerManagerInternal, + IActivityManager activityManagerInternal, + @Nullable VirtualDeviceManagerInternal virtualDeviceManagerInternal, + @Nullable AudioManager audioManager) { + int ringerMode = (audioManager == null) + ? AudioManager.RINGER_MODE_NORMAL + : audioManager.getRingerModeInternal(); + String sysUiPackage = packageManagerInternal.getSystemUiServiceComponent().getPackageName(); synchronized (mLock) { - mPowerManagerInternal = pm; - mAudioManager = am; + mPowerManagerInternal = powerManagerInternal; + mVirtualDeviceManagerInternal = virtualDeviceManagerInternal; + mAudioManager = audioManager; mRingerMode = ringerMode; + mSystemUiPackage = sysUiPackage; } try { - ActivityManager.getService().registerUidObserver(mUidObserver, + activityManagerInternal.registerUidObserver(mUidObserver, ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE, ActivityManager.PROCESS_STATE_UNKNOWN, /* callingPackage= */ null); } catch (RemoteException e) { @@ -252,32 +273,16 @@ final class VibrationSettings { } try { - ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG); + activityManagerInternal.registerUserSwitchObserver(mUserSwitchObserver, TAG); } catch (RemoteException e) { // ignored; both services live in system_server } - pm.registerLowPowerModeObserver( - new PowerManagerInternal.LowPowerModeListener() { - @Override - public int getServiceType() { - return PowerManager.ServiceType.VIBRATION; - } - - @Override - public void onLowPowerModeChanged(PowerSaveState result) { - boolean shouldNotifyListeners; - synchronized (mLock) { - shouldNotifyListeners = result.batterySaverEnabled != mBatterySaverMode; - mBatterySaverMode = result.batterySaverEnabled; - } - if (shouldNotifyListeners) { - notifyListeners(); - } - } - }); - - registerSettingsChangeReceiver(INTERNAL_RINGER_MODE_CHANGED_INTENT_FILTER); + powerManagerInternal.registerLowPowerModeObserver(mLowPowerModeListener); + + mContext.registerReceiver(mRingerModeBroadcastReceiver, + new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION), + Context.RECEIVER_EXPORTED_UNAUDITED); // Listen to all settings that might affect the result of Vibrator.getVibrationIntensity. registerSettingsObserver(Settings.System.getUriFor(Settings.System.VIBRATE_INPUT_DEVICES)); @@ -301,12 +306,7 @@ final class VibrationSettings { if (mVibrationConfig.ignoreVibrationsOnWirelessCharger()) { Intent batteryStatus = mContext.registerReceiver( - new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - updateBatteryInfo(intent); - } - }, + mBatteryBroadcastReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED), Context.RECEIVER_NOT_EXPORTED); // After registering the receiver for battery status, process the sticky broadcast that @@ -476,8 +476,10 @@ final class VibrationSettings { public boolean shouldCancelVibrationOnScreenOff(@NonNull CallerInfo callerInfo, long vibrationStartUptimeMillis) { PowerManagerInternal pm; + String sysUiPackageName; synchronized (mLock) { pm = mPowerManagerInternal; + sysUiPackageName = mSystemUiPackage; } if (pm != null) { // The SleepData from PowerManager may refer to a more recent sleep than the broadcast @@ -501,7 +503,7 @@ final class VibrationSettings { } // Only allow vibrations from System packages to continue vibrating when the screen goes off return callerInfo.uid != Process.SYSTEM_UID && callerInfo.uid != 0 - && !mSystemUiPackage.equals(callerInfo.opPkg); + && !Objects.equals(sysUiPackageName, callerInfo.opPkg); } /** @@ -782,11 +784,6 @@ final class VibrationSettings { UserHandle.USER_ALL); } - private void registerSettingsChangeReceiver(IntentFilter intentFilter) { - mContext.registerReceiver(mSettingChangeReceiver, intentFilter, - Context.RECEIVER_EXPORTED_UNAUDITED); - } - @Nullable private VibrationEffect createEffectFromResource(int resId) { return createEffectFromResource(mContext.getResources(), resId); @@ -833,12 +830,11 @@ final class VibrationSettings { } private boolean isAppRunningOnAnyVirtualDevice(int uid) { - if (mVirtualDeviceManagerInternal == null) { - mVirtualDeviceManagerInternal = - LocalServices.getService(VirtualDeviceManagerInternal.class); + VirtualDeviceManagerInternal vdm; + synchronized (mLock) { + vdm = mVirtualDeviceManagerInternal; } - return mVirtualDeviceManagerInternal != null - && mVirtualDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(uid); + return vdm != null && vdm.isAppRunningOnAnyVirtualDevice(uid); } /** Implementation of {@link ContentObserver} to be registered to a setting {@link Uri}. */ @@ -857,7 +853,7 @@ final class VibrationSettings { /** Implementation of {@link BroadcastReceiver} to update on ringer mode change. */ @VisibleForTesting - final class SettingsBroadcastReceiver extends BroadcastReceiver { + final class RingerModeBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); @@ -868,6 +864,18 @@ final class VibrationSettings { } } + /** Implementation of {@link BroadcastReceiver} to update on battery mode change. */ + @VisibleForTesting + final class BatteryBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_BATTERY_CHANGED.equals(action)) { + updateBatteryInfo(intent); + } + } + } + /** Implementation of {@link ContentObserver} to be registered to a setting {@link Uri}. */ @VisibleForTesting final class VibrationUidObserver extends UidObserver { @@ -913,4 +921,25 @@ final class VibrationSettings { update(); } } + + /** Implementation of {@link PowerManagerInternal.LowPowerModeListener} for low battery. */ + @VisibleForTesting + final class VibrationLowPowerModeListener implements PowerManagerInternal.LowPowerModeListener { + @Override + public int getServiceType() { + return PowerManager.ServiceType.VIBRATION; + } + + @Override + public void onLowPowerModeChanged(PowerSaveState result) { + boolean shouldNotifyListeners; + synchronized (mLock) { + shouldNotifyListeners = result.batterySaverEnabled != mBatterySaverMode; + mBatterySaverMode = result.batterySaverEnabled; + } + if (shouldNotifyListeners) { + notifyListeners(); + } + } + } } diff --git a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java index 5137d1938332..1d52e3c87d17 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStepConductor.java +++ b/services/core/java/com/android/server/vibrator/VibrationStepConductor.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.os.Build; import android.os.CombinedVibration; import android.os.IBinder; +import android.os.RemoteException; import android.os.VibrationEffect; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; @@ -38,6 +39,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.NoSuchElementException; import java.util.PriorityQueue; import java.util.Queue; import java.util.concurrent.CancellationException; @@ -358,6 +360,28 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { } /** + * Returns true if successfully linked this conductor to the death of the binder that requested + * the vibration. + */ + public boolean linkToDeath() { + try { + mVibration.callerToken.linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Error linking vibration to token death", e); + return false; + } + return true; + } + + public void unlinkToDeath() { + try { + mVibration.callerToken.unlinkToDeath(this, 0); + } catch (NoSuchElementException e) { + Slog.wtf(TAG, "Failed to unlink vibration to token death", e); + } + } + + /** * Notify the execution that cancellation is requested. This will be acted upon * asynchronously in the VibrationThread. * @@ -452,6 +476,23 @@ final class VibrationStepConductor implements IBinder.DeathRecipient { } } + /** + * Notify that the VibrationThread has completed the vibration effect playback. + * + * <p>This is a lightweight method intended to be called by the vibration thread directly. The + * VibrationThread may still be continuing with cleanup tasks, and should not be given new work + * until it notifies the manager that it has been released. + */ + public void notifyVibrationComplete(@NonNull Vibration.EndInfo endInfo) { + if (Build.IS_DEBUGGABLE) { + expectIsVibrationThread(true); + } + if (DEBUG) { + Slog.d(TAG, "Vibration " + mVibration.id + " finished with " + endInfo); + } + mVibration.end(endInfo); + } + /** Returns true if a cancellation signal was sent via {@link #notifyCancelled}. */ public boolean wasNotifiedToCancel() { if (Build.IS_DEBUGGABLE) { diff --git a/services/core/java/com/android/server/vibrator/VibrationThread.java b/services/core/java/com/android/server/vibrator/VibrationThread.java index 4c1e16c0d14e..5b22c109f183 100644 --- a/services/core/java/com/android/server/vibrator/VibrationThread.java +++ b/services/core/java/com/android/server/vibrator/VibrationThread.java @@ -16,12 +16,12 @@ package com.android.server.vibrator; +import static android.os.Trace.TRACE_TAG_VIBRATOR; + import android.annotation.NonNull; import android.annotation.Nullable; -import android.os.IBinder; import android.os.PowerManager; import android.os.Process; -import android.os.RemoteException; import android.os.SystemClock; import android.os.Trace; import android.os.WorkSource; @@ -31,7 +31,6 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.vibrator.VibrationSession.Status; -import java.util.NoSuchElementException; import java.util.Objects; /** Plays a {@link HalVibration} in dedicated thread. */ @@ -72,14 +71,6 @@ final class VibrationThread extends Thread { void noteVibratorOff(int uid); /** - * Tell the manager that the currently active vibration has completed its vibration, from - * the perspective of the Effect. However, the VibrationThread may still be continuing with - * cleanup tasks, and should not be given new work until {@link #onVibrationThreadReleased} - * is called. - */ - void onVibrationCompleted(long vibrationId, @NonNull Vibration.EndInfo vibrationEndInfo); - - /** * Tells the manager that the VibrationThread is finished with the previous vibration and * all of its cleanup tasks, and the vibrators can now be used for another vibration. */ @@ -128,7 +119,7 @@ final class VibrationThread extends Thread { * before the release callback. */ boolean runVibrationOnVibrationThread(VibrationStepConductor conductor) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "runVibrationOnVibrationThread"); try { synchronized (mLock) { if (mRequestedActiveConductor != null) { @@ -140,7 +131,7 @@ final class VibrationThread extends Thread { } return true; } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -243,7 +234,7 @@ final class VibrationThread extends Thread { mWakeLock.acquire(); try { try { - runCurrentVibrationWithWakeLockAndDeathLink(); + playVibration(); } finally { clientVibrationCompleteIfNotAlready( new Vibration.EndInfo(Status.FINISHED_UNEXPECTED)); @@ -254,46 +245,23 @@ final class VibrationThread extends Thread { } } - /** - * Runs the VibrationThread with the binder death link, handling link/unlink failures. - * Called from within runWithWakeLock. - */ - private void runCurrentVibrationWithWakeLockAndDeathLink() { - IBinder vibrationBinderToken = mExecutingConductor.getVibration().callerToken; - try { - vibrationBinderToken.linkToDeath(mExecutingConductor, 0); - } catch (RemoteException e) { - Slog.e(TAG, "Error linking vibration to token death", e); - clientVibrationCompleteIfNotAlready( - new Vibration.EndInfo(Status.IGNORED_ERROR_TOKEN)); - return; - } - // Ensure that the unlink always occurs now. - try { - // This is the actual execution of the vibration. - playVibration(); - } finally { - try { - vibrationBinderToken.unlinkToDeath(mExecutingConductor, 0); - } catch (NoSuchElementException e) { - Slog.wtf(TAG, "Failed to unlink token", e); - } - } - } - // Indicate that the vibration is complete. This can be called multiple times only for // convenience of handling error conditions - an error after the client is complete won't // affect the status. private void clientVibrationCompleteIfNotAlready(@NonNull Vibration.EndInfo vibrationEndInfo) { if (!mCalledVibrationCompleteCallback) { mCalledVibrationCompleteCallback = true; - mVibratorManagerHooks.onVibrationCompleted( - mExecutingConductor.getVibration().id, vibrationEndInfo); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "notifyVibrationComplete"); + try { + mExecutingConductor.notifyVibrationComplete(vibrationEndInfo); + } finally { + Trace.traceEnd(TRACE_TAG_VIBRATOR); + } } } private void playVibration() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "playVibration"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "playVibration"); try { mExecutingConductor.prepareToStart(); while (!mExecutingConductor.isFinished()) { @@ -317,7 +285,7 @@ final class VibrationThread extends Thread { } } } 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 3c478500876f..c120fc7d82f5 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 @@ final class VibratorController { /** 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 @@ final class VibratorController { } } } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -199,13 +201,13 @@ final class VibratorController { /** 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 @@ final class VibratorController { * <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 @@ final class VibratorController { mNativeWrapper.setExternalControl(externalControl); } } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -234,7 +238,7 @@ final class VibratorController { * 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 @@ final class VibratorController { } } } 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 @@ final class VibratorController { } } } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -279,7 +283,7 @@ final class VibratorController { * 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 @@ final class VibratorController { return duration; } } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -304,7 +308,7 @@ final class VibratorController { * 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 @@ final class VibratorController { return duration; } finally { vendorData.recycle(); - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } } @@ -335,7 +339,7 @@ final class VibratorController { * 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 @@ final class VibratorController { return duration; } } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -361,7 +365,7 @@ final class VibratorController { * 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 @@ final class VibratorController { return duration; } } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -388,7 +392,7 @@ final class VibratorController { * @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 @@ final class VibratorController { return duration; } } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -413,7 +417,7 @@ final class VibratorController { * <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 @@ final class VibratorController { 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 a76d8d6bded0..95c648334327 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 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @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 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mServiceReady = true; } Slog.v(TAG, "VibratorManager service initialized"); - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -413,7 +414,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @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 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } 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 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { 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 class VibratorManagerService extends IVibratorManagerService.Stub { 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 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { HalVibration vibrateWithPermissionCheck(int uid, int deviceId, String opPkg, @NonNull CombinedVibration effect, @Nullable VibrationAttributes attrs, String reason, IBinder token) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate, reason = " + reason); - try { - attrs = fixupVibrationAttributes(attrs, effect); - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.VIBRATE, "vibrate"); - return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } + attrs = fixupVibrationAttributes(attrs, effect); + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.VIBRATE, "vibrate"); + return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token); } HalVibration vibrateWithoutPermissionCheck(int uid, int deviceId, String opPkg, @NonNull CombinedVibration effect, @NonNull VibrationAttributes attrs, String reason, IBinder token) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "vibrate no perm check, reason = " + reason); - try { - return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } + return vibrateInternal(uid, deviceId, opPkg, effect, attrs, reason, token); } private HalVibration vibrateInternal(int uid, int deviceId, String opPkg, @@ -633,12 +629,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { final long ident = Binder.clearCallingIdentity(); try { if (mCurrentExternalVibration != null) { - mCurrentExternalVibration.notifyEnded(); vib.stats.reportInterruptedAnotherVibration( - mCurrentExternalVibration.callerInfo); - endExternalVibrateLocked( - new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED, - vib.callerInfo), + mCurrentExternalVibration.getCallerInfo()); + endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED, vib.callerInfo, /* continueExternalControl= */ false); } else if (mCurrentVibration != null) { if (mCurrentVibration.getVibration().canPipelineWith(vib)) { @@ -666,7 +659,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Ignored or failed to start the vibration, end it and report metrics right away. if (vibrationEndInfo != null) { - endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ true); + endVibrationLocked(vib, vibrationEndInfo); } return vib; } @@ -674,7 +667,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Override // Binder call public void cancelVibrate(int usageFilter, IBinder token) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelVibrate"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "cancelVibrate"); try { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.VIBRATE, @@ -703,16 +696,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { && shouldCancelVibration( mCurrentExternalVibration.getCallerInfo().attrs, usageFilter)) { - mCurrentExternalVibration.notifyEnded(); - endExternalVibrateLocked( - cancelledByUserInfo, /* continueExternalControl= */ false); + endExternalVibrateLocked(cancelledByUserInfo.status, + cancelledByUserInfo.endedBy, /* continueExternalControl= */ false); } } finally { Binder.restoreCallingIdentity(ident); } } } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -903,7 +895,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @GuardedBy("mLock") @Nullable private Vibration.EndInfo startVibrationLocked(HalVibration vib) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "startVibrationLocked"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "startVibrationLocked"); try { if (mInputDeviceDelegate.isAvailable()) { return startVibrationOnInputDevicesLocked(vib); @@ -923,7 +915,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mNextVibration = conductor; return null; } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -934,9 +926,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { int mode = startAppOpModeLocked(vib.callerInfo); switch (mode) { case AppOpsManager.MODE_ALLOWED: - Trace.asyncTraceBegin(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); + Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0); // Make sure mCurrentVibration is set while triggering the VibrationThread. mCurrentVibration = conductor; + if (!mCurrentVibration.linkToDeath()) { + // Shouldn't happen. The method call already logs a wtf. + mCurrentVibration = null; // Aborted. + return new Vibration.EndInfo(Status.IGNORED_ERROR_TOKEN); + } if (!mVibrationThread.runVibrationOnVibrationThread(mCurrentVibration)) { // Shouldn't happen. The method call already logs a wtf. mCurrentVibration = null; // Aborted. @@ -953,14 +950,21 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @GuardedBy("mLock") - private void endVibrationLocked(Vibration vib, Vibration.EndInfo vibrationEndInfo, - boolean shouldWriteStats) { + private void endVibrationLocked(Vibration vib, Status status) { + endVibrationLocked(vib, new Vibration.EndInfo(status)); + } + + @GuardedBy("mLock") + private void endVibrationLocked(Vibration vib, Vibration.EndInfo vibrationEndInfo) { vib.end(vibrationEndInfo); + reportEndedVibrationLocked(vib); + } + + @GuardedBy("mLock") + private void reportEndedVibrationLocked(Vibration vib) { logAndRecordVibration(vib.getDebugInfo()); - if (shouldWriteStats) { - mFrameworkStatsLogger.writeVibrationReportedAsync( - vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis())); - } + mFrameworkStatsLogger.writeVibrationReportedAsync( + vib.getStatsInfo(/* completionUptimeMillis= */ SystemClock.uptimeMillis())); } private VibrationStepConductor createVibrationStepConductor(HalVibration vib) { @@ -1055,17 +1059,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @GuardedBy("mLock") - private void reportFinishedVibrationLocked(Vibration.EndInfo vibrationEndInfo) { + private void reportFinishedVibrationLocked() { Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); + mCurrentVibration.unlinkToDeath(); HalVibration vib = mCurrentVibration.getVibration(); if (DEBUG) { - Slog.d(TAG, "Reporting vibration " + vib.id + " finished with " - + vibrationEndInfo); + Slog.d(TAG, "Reporting vibration " + vib.id + " finished with " + vib.getStatus()); } - // DO NOT write metrics at this point, wait for the VibrationThread to report the - // vibration was released, after all cleanup. The metrics will be reported then. - endVibrationLocked(vib, vibrationEndInfo, /* shouldWriteStats= */ false); finishAppOpModeLocked(vib.callerInfo); + reportEndedVibrationLocked(vib); } private void onSyncedVibrationComplete(long vibrationId) { @@ -1575,7 +1577,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Override public boolean prepareSyncedVibration(long requiredCapabilities, int[] vibratorIds) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "prepareSyncedVibration"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "prepareSyncedVibration"); try { if ((mCapabilities & requiredCapabilities) != requiredCapabilities) { // This sync step requires capabilities this device doesn't have, skipping @@ -1584,33 +1586,33 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } return mNativeWrapper.prepareSynced(vibratorIds); } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @Override public boolean triggerSyncedVibration(long vibrationId) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "triggerSyncedVibration"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "triggerSyncedVibration"); try { return mNativeWrapper.triggerSynced(vibrationId); } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @Override public void cancelSyncedVibration() { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "cancelSyncedVibration"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "cancelSyncedVibration"); try { mNativeWrapper.cancelSynced(); } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @Override public void noteVibratorOn(int uid, long duration) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOn"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "noteVibratorOn"); try { if (duration <= 0) { // Tried to turn vibrator ON and got: @@ -1629,38 +1631,20 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } catch (RemoteException e) { Slog.e(TAG, "Error logging VibratorStateChanged to ON", e); } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @Override public void noteVibratorOff(int uid) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "noteVibratorOff"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "noteVibratorOff"); try { mBatteryStatsService.noteVibratorOff(uid); mFrameworkStatsLogger.writeVibratorStateOffAsync(uid); } catch (RemoteException e) { Slog.e(TAG, "Error logging VibratorStateChanged to OFF", e); } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); - } - } - - @Override - public void onVibrationCompleted(long vibrationId, Vibration.EndInfo vibrationEndInfo) { - if (DEBUG) { - Slog.d(TAG, "Vibration " + vibrationId + " finished with " + vibrationEndInfo); - } - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationCompleted"); - try { - synchronized (mLock) { - if (mCurrentVibration != null - && mCurrentVibration.getVibration().id == vibrationId) { - reportFinishedVibrationLocked(vibrationEndInfo); - } - } - } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -1669,7 +1653,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (DEBUG) { Slog.d(TAG, "VibrationThread released after finished vibration"); } - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onVibrationThreadReleased: " + vibrationId); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVibrationThreadReleased"); + try { synchronized (mLock) { if (DEBUG) { @@ -1682,11 +1667,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { mCurrentVibration.getVibration().id, vibrationId)); } if (mCurrentVibration != null) { - // This is when we consider the current vibration complete, so report - // metrics. - mFrameworkStatsLogger.writeVibrationReportedAsync( - mCurrentVibration.getVibration().getStatsInfo( - /* completionUptimeMillis= */ SystemClock.uptimeMillis())); + // This is when we consider the current vibration complete, report metrics. + reportFinishedVibrationLocked(); mCurrentVibration = null; } if (mNextVibration != null) { @@ -1696,13 +1678,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { nextConductor); if (vibrationEndInfo != null) { // Failed to start the vibration, end it and report metrics right away. - endVibrationLocked(nextConductor.getVibration(), - vibrationEndInfo, /* shouldWriteStats= */ true); + endVibrationLocked(nextConductor.getVibration(), vibrationEndInfo); } } } } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } } @@ -1929,8 +1910,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { + " with end info: " + vibrationEndInfo); } // Clearing next vibration before playing it, end it and report metrics right away. - endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo, - /* shouldWriteStats= */ true); + endVibrationLocked(mNextVibration.getVibration(), vibrationEndInfo); mNextVibration = null; } } @@ -1938,23 +1918,24 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { /** * Ends the external vibration, and clears related service state. * - * @param vibrationEndInfo the status and related info to end the associated Vibration + * @param status the status to end the associated Vibration + * @param endedBy the caller that caused this vibration to end * @param continueExternalControl indicates whether external control will continue. If not, the * HAL will have external control turned off. */ @GuardedBy("mLock") - private void endExternalVibrateLocked(Vibration.EndInfo vibrationEndInfo, + private void endExternalVibrateLocked(Status status, CallerInfo endedBy, boolean continueExternalControl) { if (mCurrentExternalVibration == null) { return; } + mCurrentExternalVibration.requestEnd(status, endedBy, /* immediate= */ true); mCurrentExternalVibration.unlinkToDeath(); if (!continueExternalControl) { setExternalControl(false, mCurrentExternalVibration.stats); } // The external control was turned off, end it and report metrics right away. - endVibrationLocked(mCurrentExternalVibration, vibrationEndInfo, - /* shouldWriteStats= */ true); + reportEndedVibrationLocked(mCurrentExternalVibration); mCurrentExternalVibration = null; } @@ -2010,7 +1991,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @Override public ExternalVibrationScale onExternalVibrationStart(ExternalVibration vib) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStart"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStart"); try { // Create Vibration.Stats as close to the received request as possible, for // tracking. @@ -2022,9 +2003,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { synchronized (mLock) { if (!hasExternalControlCapability()) { - endVibrationLocked(externalVibration, - new Vibration.EndInfo(Status.IGNORED_UNSUPPORTED), - /* shouldWriteStats= */ true); + endVibrationLocked(externalVibration, Status.IGNORED_UNSUPPORTED); return externalVibration.getScale(); } @@ -2035,9 +2014,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Slog.w(TAG, "pkg=" + vib.getPackage() + ", uid=" + vib.getUid() + " tried to play externally controlled vibration" + " without VIBRATE permission, ignoring."); - endVibrationLocked(externalVibration, - new Vibration.EndInfo(Status.IGNORED_MISSING_PERMISSION), - /* shouldWriteStats= */ true); + endVibrationLocked(externalVibration, Status.IGNORED_MISSING_PERMISSION); return externalVibration.getScale(); } @@ -2058,8 +2035,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } if (vibrationEndInfo != null) { - endVibrationLocked(externalVibration, vibrationEndInfo, - /* shouldWriteStats= */ true); + endVibrationLocked(externalVibration, vibrationEndInfo); return externalVibration.getScale(); } @@ -2092,41 +2068,20 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // as we would need to mute the old one still if it came from a different // controller. alreadyUnderExternalControl = true; - mCurrentExternalVibration.notifyEnded(); externalVibration.stats.reportInterruptedAnotherVibration( - mCurrentExternalVibration.callerInfo); - endExternalVibrateLocked( - new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED, - externalVibration.callerInfo), - /* continueExternalControl= */ true); + mCurrentExternalVibration.getCallerInfo()); + endExternalVibrateLocked(Status.CANCELLED_SUPERSEDED, + externalVibration.callerInfo, /* continueExternalControl= */ true); } - - VibrationAttributes attrs = fixupVibrationAttributes( - vib.getVibrationAttributes(), - /* effect= */ null); - if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) { - // Force update of user settings before checking if this vibration effect - // should be ignored or scaled. - mVibrationSettings.update(); - } - - mCurrentExternalVibration = externalVibration; - externalVibration.linkToDeath(this::onExternalVibrationBinderDied); - externalVibration.scale(mVibrationScaler, attrs.getUsage()); } - + // Wait for lock and interact with HAL to set external control outside main lock. if (waitForCompletion) { if (!mVibrationThread.waitForThreadIdle(VIBRATION_CANCEL_WAIT_MILLIS)) { Slog.e(TAG, "Timed out waiting for vibration to cancel"); synchronized (mLock) { - // Trigger endExternalVibrateLocked to unlink to death recipient. - endExternalVibrateLocked( - new Vibration.EndInfo(Status.IGNORED_ERROR_CANCELLING), - /* continueExternalControl= */ false); - // Mute the request, vibration will be ignored. - externalVibration.muteScale(); + endVibrationLocked(externalVibration, Status.IGNORED_ERROR_CANCELLING); + return externalVibration.getScale(); } - return externalVibration.getScale(); } } if (!alreadyUnderExternalControl) { @@ -2135,21 +2090,36 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } setExternalControl(true, externalVibration.stats); } - if (DEBUG) { - Slog.d(TAG, "Playing external vibration: " + vib); + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "Playing external vibration: " + vib); + } + VibrationAttributes attrs = fixupVibrationAttributes( + vib.getVibrationAttributes(), + /* effect= */ null); + if (attrs.isFlagSet(VibrationAttributes.FLAG_INVALIDATE_SETTINGS_CACHE)) { + // Force update of user settings before checking if this vibration effect + // should be ignored or scaled. + mVibrationSettings.update(); + } + + mCurrentExternalVibration = externalVibration; + externalVibration.linkToDeath(this::onExternalVibrationBinderDied); + externalVibration.scale(mVibrationScaler, attrs.getUsage()); + + // Vibrator will start receiving data from external channels after this point. + // Report current time as the vibration start time, for debugging. + externalVibration.stats.reportStarted(); + return externalVibration.getScale(); } - // Vibrator will start receiving data from external channels after this point. - // Report current time as the vibration start time, for debugging. - externalVibration.stats.reportStarted(); - return externalVibration.getScale(); } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @Override public void onExternalVibrationStop(ExternalVibration vib) { - Trace.traceBegin(Trace.TRACE_TAG_VIBRATOR, "onExternalVibrationStop"); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStop"); try { synchronized (mLock) { if (mCurrentExternalVibration != null @@ -2157,13 +2127,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (DEBUG) { Slog.d(TAG, "Stopping external vibration: " + vib); } - endExternalVibrateLocked( - new Vibration.EndInfo(Status.FINISHED), + endExternalVibrateLocked(Status.FINISHED, /* endedBy= */ null, /* continueExternalControl= */ false); } } } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIBRATOR); + Trace.traceEnd(TRACE_TAG_VIBRATOR); } } @@ -2182,8 +2151,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (DEBUG) { Slog.d(TAG, "External vibration finished because binder died"); } - endExternalVibrateLocked( - new Vibration.EndInfo(Status.CANCELLED_BINDER_DIED), + endExternalVibrateLocked(Status.CANCELLED_BINDER_DIED, /* endedBy= */ null, /* continueExternalControl= */ false); } } @@ -2232,32 +2200,39 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { @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 232c3b62bcef..dcf031953610 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 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal * the application did not handle. */ @Override - public KeyEvent dispatchUnhandledKey( - IBinder focusedToken, KeyEvent event, int policyFlags) { - return mService.mPolicy.dispatchUnhandledKey(focusedToken, event, policyFlags); + public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) { + return mService.mPolicy.interceptUnhandledKey(event, focusedToken); } /** Callback to get pointer layer. */ diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index ddbfd70ea4c4..d7dc4597c508 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -222,7 +222,8 @@ final class InputMonitor { UserHandle clientUser) { final InputConsumerImpl existingConsumer = getInputConsumer(name); if (existingConsumer != null && existingConsumer.mClientUser.equals(clientUser)) { - throw new IllegalStateException("Existing input consumer found with name: " + name + destroyInputConsumer(existingConsumer.mToken); + Slog.w(TAG_WM, "Replacing existing input consumer found with name: " + name + ", display: " + mDisplayId + ", user: " + clientUser); } diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 9da848aa05d8..bf623b2e2105 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_DREAM; 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 @@ class RecentTasks { 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 5cd117b512d4..efca90217e83 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 @@ public: 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 @@ private: // 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 @@ private: 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 @@ NativeInputManager::~NativeInputManager() { 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 @@ void NativeInputManager::requestPointerCapture(const sp<IBinder>& windowToken, b 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 @@ void NativeInputManager::interceptKeyBeforeQueueing(const KeyEvent& keyEvent, // - 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 @@ void NativeInputManager::interceptMotionBeforeQueueing(ui::LogicalDisplayId disp // - 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 @@ void NativeInputManager::handleInterceptActions(jint wmActions, nsecs_t when, } } +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 @@ static void nativeSetShowTouches(JNIEnv* env, jobject nativeImplObj, jboolean en 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 @@ static const JNINativeMethod gInputManagerMethods[] = { (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 0eafb59bdeac..a07facf79423 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 355b0ab15a62..5309263ed87c 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 @@ package com.android.server.display.config { 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 407a5a638db1..4e89b85305d1 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (mOwners.hasDeviceOwner()) { return false; } - + final ComponentName profileOwner = getProfileOwnerAsUser(userId); if (profileOwner == null) { return false; @@ -11371,7 +11373,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { if (isManagedProfile(userId)) { return false; } - + return true; } private void enforceCanQueryLockTaskLocked(ComponentName who, String callerPackageName) { @@ -18213,6 +18215,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { return false; } + @GuardedBy("getLockObject()") private boolean areAllUsersAffiliatedWithDeviceLocked() { return mInjector.binderWithCleanCallingIdentity(() -> { final List<UserInfo> userInfos = mUserManager.getAliveUsers(); @@ -18310,10 +18313,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { isCallerDelegate(caller, DELEGATION_SECURITY_LOGGING)); } - Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile() - || areAllUsersAffiliatedWithDeviceLocked()); + synchronized (getLockObject()) { + Preconditions.checkCallAuthorization(isOrganizationOwnedDeviceWithManagedProfile() + || areAllUsersAffiliatedWithDeviceLocked()); + } } DevicePolicyEventLogger @@ -19384,11 +19391,13 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { 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 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } }); } - + + @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 000000000000..fead05bc7e49 --- /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 7ffd0eca9b96..4c515c173c8d 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 @@ package com.android.server.supervision; 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 @@ public class SupervisionService extends ISupervisionManager.Stub { 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 @@ public class SupervisionService extends ISupervisionManager.Stub { } @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; + + 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; + } - fout.println("Supervision enabled: " + isSupervisionEnabled()); + @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 @@ public class SupervisionService extends ISupervisionManager.Stub { @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 3aba24a3d4a5..2adaae3943f1 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 @@ public class SupervisionServiceShellCommand extends ShellCommand { 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 000000000000..56162372f740 --- /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 e5d315358df6..72cbac331551 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 @@ import java.io.StringWriter; 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 @@ public final class InputMethodManagerServiceTests { 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 5c4716dc751e..7d5532f6e401 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.BeforeClass 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 @@ class PackageManagerComponentLabelIconOverrideTest { 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 b21c34905bad..2144785ed8fd 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 @@ class PackageStateTest { 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 fd05b26c320b..8e1be9a777fd 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 final class DisplayDeviceConfigTest { 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 final class DisplayDeviceConfigTest { 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 final class DisplayDeviceConfigTest { 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 @@ public final class DisplayDeviceConfigTest { @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 @@ public final class DisplayDeviceConfigTest { @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 @@ public final class DisplayDeviceConfigTest { 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 @@ public final class DisplayDeviceConfigTest { 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 @@ public final class DisplayDeviceConfigTest { 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 120cc84193cd..f5bed999d5a0 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.assertFalse; 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.Binder; 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.dx.mockito.inline.extended.StaticMockitoSession; 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 @@ public class LocalDisplayAdapterTest { private DisplayManagerFlags mFlags; @Mock private DisplayPowerController mMockedDisplayPowerController; + @Mock + private ColorDisplayService.ColorDisplayServiceInternal mMockedColorDisplayServiceInternal; private Handler mHandler; @@ -133,6 +139,11 @@ public class LocalDisplayAdapterTest { 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 @@ public class LocalDisplayAdapterTest { 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 @@ public class LocalDisplayAdapterTest { 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 @@ public class LocalDisplayAdapterTest { } } + @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 @@ public class LocalDisplayAdapterTest { 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 fc4d8d871fd5..07029268661e 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.verifyNoMoreInteractions; 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.WorkSource; 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 @@ import java.util.concurrent.Executor; 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 @@ public class NotifierTest { @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 @@ public class NotifierTest { private FakeExecutor mTestExecutor = new FakeExecutor(); private Notifier mNotifier; + private DisplayInfo mDefaultDisplayInfo = new DisplayInfo(); + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -103,11 +125,25 @@ public class NotifierTest { 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 @@ public class NotifierTest { } @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 @@ public class NotifierTest { mContextSpy, mBatteryStats, mInjector.createSuspendBlocker(mService, "testBlocker"), - null, + mPolicy, null, null, mTestExecutor, mPowerManagerFlags, injector); diff --git a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml index fd55428c48df..c3df0785bd9b 100644 --- a/services/tests/powerstatstests/res/xml/irq_device_map_3.xml +++ b/services/tests/powerstatstests/res/xml/irq_device_map_3.xml @@ -32,4 +32,7 @@ <device name="test.sensor.device"> <subsystem>Sensor</subsystem> </device> + <device name="test.bluetooth.device"> + <subsystem>Bluetooth</subsystem> + </device> </irq-device-map>
\ No newline at end of file diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java index 1d20538724a8..c037f97e34c9 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 @@ public class BatteryStatsImplTest { 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 fde84e967c98..0e60156aecd1 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 @@ public class BatteryUsageStatsProviderTest { 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 @@ public class BatteryUsageStatsProviderTest { .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 @@ public class BatteryUsageStatsProviderTest { return null; }).when(powerStatsStore).storeBatteryUsageStats(anyLong(), any()); - mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore); + mStatsRule.getBatteryStats().saveBatteryUsageStatsOnReset(provider, powerStatsStore, + /* accumulateBatteryUsageStats */ false); // Make an incompatible change of supported energy components. This will trigger // a BatteryStats reset, which will generate a snapshot of battery stats. diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java index 0dc836ba0400..fe4d971face5 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/wakeups/CpuWakeupStatsTest.java @@ -17,6 +17,7 @@ package com.android.server.power.stats.wakeups; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_ALARM; +import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_BLUETOOTH; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SENSOR; import static android.os.BatteryStatsInternal.CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER; @@ -52,6 +53,7 @@ public class CpuWakeupStatsTest { private static final String KERNEL_REASON_SOUND_TRIGGER_IRQ = "129 test.sound_trigger.device"; private static final String KERNEL_REASON_SENSOR_IRQ = "15 test.sensor.device"; private static final String KERNEL_REASON_CELLULAR_DATA_IRQ = "18 test.cellular_data.device"; + private static final String KERNEL_REASON_BLUETOOTH_IRQ = "19 test.bluetooth.device"; private static final String KERNEL_REASON_UNKNOWN_IRQ = "140 test.unknown.device"; private static final String KERNEL_REASON_UNKNOWN_FORMAT = "free-form-reason test.alarm.device"; private static final String KERNEL_REASON_ALARM_ABNORMAL = "-1 test.alarm.device"; @@ -62,12 +64,14 @@ public class CpuWakeupStatsTest { private static final int TEST_UID_3 = 92261423; private static final int TEST_UID_4 = 56926423; private static final int TEST_UID_5 = 76421423; + private static final int TEST_UID_6 = 62345353; private static final int TEST_PROC_STATE_1 = 72331; private static final int TEST_PROC_STATE_2 = 792351; private static final int TEST_PROC_STATE_3 = 138831; private static final int TEST_PROC_STATE_4 = 23231; private static final int TEST_PROC_STATE_5 = 42; + private static final int TEST_PROC_STATE_6 = 129942; private static final Context sContext = InstrumentationRegistry.getTargetContext(); private final Handler mHandler = Mockito.mock(Handler.class); @@ -79,6 +83,7 @@ public class CpuWakeupStatsTest { obj.mUidProcStates.put(TEST_UID_3, TEST_PROC_STATE_3); obj.mUidProcStates.put(TEST_UID_4, TEST_PROC_STATE_4); obj.mUidProcStates.put(TEST_UID_5, TEST_PROC_STATE_5); + obj.mUidProcStates.put(TEST_UID_6, TEST_PROC_STATE_6); } @Test @@ -118,6 +123,7 @@ public class CpuWakeupStatsTest { CPU_WAKEUP_SUBSYSTEM_SOUND_TRIGGER, CPU_WAKEUP_SUBSYSTEM_SENSOR, CPU_WAKEUP_SUBSYSTEM_CELLULAR_DATA, + CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, }; final String[] kernelReasons = new String[] { @@ -126,10 +132,11 @@ public class CpuWakeupStatsTest { KERNEL_REASON_SOUND_TRIGGER_IRQ, KERNEL_REASON_SENSOR_IRQ, KERNEL_REASON_CELLULAR_DATA_IRQ, + KERNEL_REASON_BLUETOOTH_IRQ, }; final int[] uids = new int[] { - TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5 + TEST_UID_2, TEST_UID_3, TEST_UID_4, TEST_UID_1, TEST_UID_5, TEST_UID_6 }; final int[] procStates = new int[] { @@ -137,7 +144,8 @@ public class CpuWakeupStatsTest { TEST_PROC_STATE_3, TEST_PROC_STATE_4, TEST_PROC_STATE_1, - TEST_PROC_STATE_5 + TEST_PROC_STATE_5, + TEST_PROC_STATE_6 }; final int total = subsystems.length; @@ -285,6 +293,40 @@ public class CpuWakeupStatsTest { } @Test + public void bluetoothIrqAttributionSolo() { + final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); + final long wakeupTime = 1236121; + + populateDefaultProcStates(obj); + + obj.noteWakeupTimeAndReason(wakeupTime, 1, KERNEL_REASON_BLUETOOTH_IRQ); + + // Outside the window, so should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_1); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, + wakeupTime + obj.mConfig.WAKEUP_MATCHING_WINDOW_MS + 1, TEST_UID_2); + // Should be attributed + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 5, TEST_UID_3, + TEST_UID_5); + + final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime); + assertThat(attribution).isNotNull(); + assertThat(attribution.size()).isEqualTo(1); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey( + TEST_UID_1)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey( + TEST_UID_2)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo( + TEST_PROC_STATE_3); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).indexOfKey( + TEST_UID_4)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo( + TEST_PROC_STATE_5); + } + + @Test public void alarmAndWifiIrqAttribution() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long wakeupTime = 92123210; @@ -400,6 +442,47 @@ public class CpuWakeupStatsTest { } @Test + public void unknownAndBluetoothAttribution() { + final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); + final long wakeupTime = 92123520; + + populateDefaultProcStates(obj); + + obj.noteWakeupTimeAndReason(wakeupTime, 24, + KERNEL_REASON_UNKNOWN_IRQ + ":" + KERNEL_REASON_BLUETOOTH_IRQ); + + // Bluetooth activity + // Outside the window, so should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, + wakeupTime - obj.mConfig.WAKEUP_MATCHING_WINDOW_MS - 1, TEST_UID_4); + // Should be attributed + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime + 2, TEST_UID_1); + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH, wakeupTime - 1, TEST_UID_3, + TEST_UID_5); + + // Unrelated, should be ignored. + obj.noteWakingActivity(CPU_WAKEUP_SUBSYSTEM_ALARM, wakeupTime + 5, TEST_UID_3); + + final SparseArray<SparseIntArray> attribution = obj.mWakeupAttribution.get(wakeupTime); + assertThat(attribution).isNotNull(); + assertThat(attribution.size()).isEqualTo(2); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_1)).isEqualTo( + TEST_PROC_STATE_1); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH) + .indexOfKey(TEST_UID_2)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_3)).isEqualTo( + TEST_PROC_STATE_3); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH) + .indexOfKey(TEST_UID_4)).isLessThan(0); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_BLUETOOTH).get(TEST_UID_5)).isEqualTo( + TEST_PROC_STATE_5); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isTrue(); + assertThat(attribution.get(CPU_WAKEUP_SUBSYSTEM_UNKNOWN)).isNull(); + assertThat(attribution.contains(CPU_WAKEUP_SUBSYSTEM_ALARM)).isFalse(); + } + + @Test public void unknownFormatWakeupIgnored() { final CpuWakeupStats obj = new CpuWakeupStats(sContext, R.xml.irq_device_map_3, mHandler); final long wakeupTime = 72123210; diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index bbe0755b9cc9..ac1b7c6876f7 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -54,6 +54,7 @@ android_test { "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 6e6d5a870031..8dfd54fe38bc 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 @@ public class AbstractAccessibilityServiceConnectionTest { @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 @@ public class AbstractAccessibilityServiceConnectionTest { 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 @@ public class AbstractAccessibilityServiceConnectionTest { 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 566feb7e3d80..7481fc8ec46d 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 static org.mockito.Mockito.when; 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 @@ public class AccessibilityManagerServiceTest { @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 @@ public class AccessibilityManagerServiceTest { 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 @@ public class AccessibilityManagerServiceTest { .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 @@ public class AccessibilityManagerServiceTest { 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 @@ public class AccessibilityManagerServiceTest { 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 b9ce8ad0b018..0c92abce7254 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 @@ public class AccountManagerServiceTest extends AndroidTestCase { 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 @@ public class AccountManagerServiceTest extends AndroidTestCase { 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 @@ public class AccountManagerServiceTest extends AndroidTestCase { + (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 464515632997..3e2949d60183 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 @@ public class AudioDeviceVolumeManagerTest { } @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 @@ public class AudioDeviceVolumeManagerTest { 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 @@ public class AudioDeviceVolumeManagerTest { 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 @@ public class AudioDeviceVolumeManagerTest { 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 beed0a3d413c..c305fd92cfbb 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.media.audio.Flags.autoPublicVolumeApiHardening; 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 @@ public class VolumeHelperTest { @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 @@ public class VolumeHelperTest { 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 @@ public class VolumeHelperTest { 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 @@ public class VolumeHelperTest { 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 f91f77a56385..cdfc521dff13 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 @@ public class PersistentDataBlockServiceTest { private File mDataBlockFile; private File mFrpSecretFile; private File mFrpSecretTmpFile; - private String mOemUnlockPropertyValue; private boolean mIsUpgradingFromPreV = false; @Mock private UserManager mUserManager; @@ -105,13 +104,6 @@ public class PersistentDataBlockServiceTest { } @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 @@ public class PersistentDataBlockServiceTest { mInterface.setOemUnlockEnabled(true); assertThat(mInterface.getOemUnlockEnabled()).isTrue(); - assertThat(mOemUnlockPropertyValue).isEqualTo("1"); } @Test @@ -635,7 +626,6 @@ public class PersistentDataBlockServiceTest { // 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 @@ public class PersistentDataBlockServiceTest { 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 000000000000..6bd4279152ab --- /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 130690d80b70..6c9015d72d5a 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(r); mService.addEnqueuedNotification(r1); - mBinderService.setCanBePromoted(mPkg, mUid, true); + mBinderService.setCanBePromoted(mPkg, mUid, true, true); waitForIdle(); @@ -16632,7 +16632,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.addNotification(r); mService.addEnqueuedNotification(r1); - mBinderService.setCanBePromoted(mPkg, mUid, false); + mBinderService.setCanBePromoted(mPkg, mUid, false, true); waitForIdle(); @@ -16728,12 +16728,12 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { 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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { .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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @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 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @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 7d63062784f9..a0c0df8853f9 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_ALL_NOTIFS_NEED_TTL; 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 @@ public class PreferencesHelperTest extends UiServiceTestCase { 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 @@ public class PreferencesHelperTest extends UiServiceTestCase { 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 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test - @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) public void testNoAppHasPermissionToPromoteByDefault() { mHelper.setShowBadge(PKG_P, UID_P, true); assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse(); } @Test - @EnableFlags(android.app.Flags.FLAG_UI_RICH_ONGOING) + @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) public void testSetCanBePromoted() { - mHelper.setCanBePromoted(PKG_P, UID_P, true); + mHelper.setCanBePromoted(PKG_P, UID_P, true, true); assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue(); - mHelper.setCanBePromoted(PKG_P, UID_P, false); + mHelper.setCanBePromoted(PKG_P, UID_P, false, true); assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isFalse(); verify(mHandler, never()).requestSort(); } + + @Test + @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) + public void testSetCanBePromoted_allowlistNotOverrideUser() { + mHelper.setCanBePromoted(PKG_P, UID_P, true, true); + assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue(); + + mHelper.setCanBePromoted(PKG_P, UID_P, false, false); + assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue(); + } } diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java index c7a136ad0f9f..23ee893e309a 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java @@ -44,7 +44,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -52,12 +53,14 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.app.IActivityManager; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManagerInternal; import android.media.AudioManager; import android.os.Handler; @@ -79,12 +82,10 @@ import androidx.test.InstrumentationRegistry; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; -import com.android.server.LocalServices; import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.vibrator.VibrationSession.CallerInfo; import com.android.server.vibrator.VibrationSession.Status; -import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -101,8 +102,7 @@ public class VibrationSettingsTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - private static final int OLD_USER_ID = 123; - private static final int NEW_USER_ID = 456; + private static final int USER_ID = 123; private static final int UID = 1; private static final int VIRTUAL_DEVICE_ID = 1; private static final String SYSUI_PACKAGE_NAME = "sysui"; @@ -132,13 +132,12 @@ public class VibrationSettingsTest { @Mock private VirtualDeviceManagerInternal mVirtualDeviceManagerInternalMock; @Mock private PackageManagerInternal mPackageManagerInternalMock; @Mock private AudioManager mAudioManagerMock; + @Mock private IActivityManager mActivityManagerMock; @Mock private VibrationConfig mVibrationConfigMock; private TestLooper mTestLooper; private ContextWrapper mContextSpy; private VibrationSettings mVibrationSettings; - private PowerManagerInternal.LowPowerModeListener mRegisteredPowerModeListener; - private BroadcastReceiver mRegisteredBatteryBroadcastReceiver; @Before public void setUp() throws Exception { @@ -146,24 +145,21 @@ public class VibrationSettingsTest { mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getContext())); ContentResolver contentResolver = mSettingsProviderRule.mockContentResolver(mContextSpy); - when(mContextSpy.getContentResolver()).thenReturn(contentResolver); - when(mContextSpy.getSystemService(eq(Context.AUDIO_SERVICE))).thenReturn(mAudioManagerMock); - doAnswer(invocation -> { - mRegisteredPowerModeListener = invocation.getArgument(0); - return null; - }).when(mPowerManagerInternalMock).registerLowPowerModeObserver(any()); + doReturn(contentResolver).when(mContextSpy).getContentResolver(); + + // Make sure broadcast receivers are not registered for this test, to avoid flakes. + doReturn(null).when(mContextSpy) + .registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class), anyInt()); + when(mPackageManagerInternalMock.getSystemUiServiceComponent()) .thenReturn(new ComponentName(SYSUI_PACKAGE_NAME, "")); - removeServicesForTest(); - addServicesForTest(); - setDefaultIntensity(VIBRATION_INTENSITY_MEDIUM); setIgnoreVibrationsOnWirelessCharger(false); - createSystemReadyVibrationSettings(); - mockGoToSleep(/* goToSleepTime= */ 0, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); + + createSystemReadyVibrationSettings(); } private void createSystemReadyVibrationSettings() { @@ -177,37 +173,18 @@ public class VibrationSettingsTest { setUserSetting(Settings.System.APPLY_RAMPING_RINGER, 0); setRingerMode(AudioManager.RINGER_MODE_NORMAL); - mVibrationSettings.onSystemReady(); - } - - private void removeServicesForTest() { - LocalServices.removeServiceForTest(PowerManagerInternal.class); - LocalServices.removeServiceForTest(PackageManagerInternal.class); - LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class); - } - - private void addServicesForTest() { - LocalServices.addService(PowerManagerInternal.class, mPowerManagerInternalMock); - LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock); - LocalServices.addService(VirtualDeviceManagerInternal.class, - mVirtualDeviceManagerInternalMock); - } - - @After - public void tearDown() throws Exception { - removeServicesForTest(); + mVibrationSettings.onSystemReady(mPackageManagerInternalMock, mPowerManagerInternalMock, + mActivityManagerMock, mVirtualDeviceManagerInternalMock, mAudioManagerMock); } @Test public void create_withOnlyRequiredSystemServices() { - // The only core services that we depend on are PowerManager and PackageManager - removeServicesForTest(); - LocalServices.addService(PowerManagerInternal.class, mPowerManagerInternalMock); - LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock); - VibrationSettings minimalVibrationSettings = new VibrationSettings(mContextSpy, new Handler(mTestLooper.getLooper()), mVibrationConfigMock); - minimalVibrationSettings.onSystemReady(); + + // The only core services that we depend on are Power, Package and Activity managers + minimalVibrationSettings.onSystemReady(mPackageManagerInternalMock, + mPowerManagerInternalMock, mActivityManagerMock, null, null); } @Test @@ -215,8 +192,8 @@ public class VibrationSettingsTest { mVibrationSettings.addListener(mListenerMock); // Testing the broadcast flow manually. - mVibrationSettings.mUserSwitchObserver.onUserSwitching(NEW_USER_ID); - mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(NEW_USER_ID); + mVibrationSettings.mUserSwitchObserver.onUserSwitching(USER_ID); + mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(USER_ID); verify(mListenerMock, times(2)).onChange(); } @@ -226,9 +203,9 @@ public class VibrationSettingsTest { mVibrationSettings.addListener(mListenerMock); // Testing the broadcast flow manually. - mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy, + mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy, new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)); - mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy, + mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy, new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)); verify(mListenerMock, times(2)).onChange(); @@ -250,9 +227,9 @@ public class VibrationSettingsTest { mVibrationSettings.addListener(mListenerMock); // Testing the broadcast flow manually. - mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); - mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); - mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); // No change. + mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); + mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); + mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); // Noop. verify(mListenerMock, times(2)).onChange(); } @@ -268,9 +245,9 @@ public class VibrationSettingsTest { // Trigger multiple observers manually. mVibrationSettings.mSettingObserver.onChange(false); - mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); - mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(NEW_USER_ID); - mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy, + mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); + mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(USER_ID); + mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy, new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)); verifyNoMoreInteractions(mListenerMock); @@ -311,11 +288,12 @@ public class VibrationSettingsTest { @Test public void wirelessChargingVibrationsEnabled_doesNotRegisterBatteryReceiver_allowsAnyUsage() { - setBatteryReceiverRegistrationResult(getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS)); setIgnoreVibrationsOnWirelessCharger(false); createSystemReadyVibrationSettings(); - assertNull(mRegisteredBatteryBroadcastReceiver); + verify(mContextSpy, never()).registerReceiver(any(BroadcastReceiver.class), + argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt()); + for (int usage : ALL_USAGES) { assertVibrationNotIgnoredForUsage(usage); } @@ -323,7 +301,6 @@ public class VibrationSettingsTest { @Test public void shouldIgnoreVibration_noBatteryIntentWhenSystemReady_allowsAnyUsage() { - setBatteryReceiverRegistrationResult(null); setIgnoreVibrationsOnWirelessCharger(true); createSystemReadyVibrationSettings(); @@ -335,7 +312,10 @@ public class VibrationSettingsTest { @Test public void shouldIgnoreVibration_onNonWirelessChargerWhenSystemReady_allowsAnyUsage() { Intent nonWirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_USB); - setBatteryReceiverRegistrationResult(nonWirelessChargingIntent); + doReturn(nonWirelessChargingIntent).when(mContextSpy).registerReceiver( + any(BroadcastReceiver.class), + argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt()); + setIgnoreVibrationsOnWirelessCharger(true); createSystemReadyVibrationSettings(); @@ -347,7 +327,10 @@ public class VibrationSettingsTest { @Test public void shouldIgnoreVibration_onWirelessChargerWhenSystemReady_doesNotAllowFromAnyUsage() { Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS); - setBatteryReceiverRegistrationResult(wirelessChargingIntent); + doReturn(wirelessChargingIntent).when(mContextSpy).registerReceiver( + any(BroadcastReceiver.class), + argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt()); + setIgnoreVibrationsOnWirelessCharger(true); createSystemReadyVibrationSettings(); @@ -358,13 +341,12 @@ public class VibrationSettingsTest { @Test public void shouldIgnoreVibration_receivesWirelessChargingIntent_doesNotAllowFromAnyUsage() { - Intent nonWirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_USB); - setBatteryReceiverRegistrationResult(nonWirelessChargingIntent); setIgnoreVibrationsOnWirelessCharger(true); createSystemReadyVibrationSettings(); Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS); - mRegisteredBatteryBroadcastReceiver.onReceive(mContextSpy, wirelessChargingIntent); + mVibrationSettings.mBatteryBroadcastReceiver.onReceive( + mContextSpy, wirelessChargingIntent); for (int usage : ALL_USAGES) { assertVibrationIgnoredForUsage(usage, Status.IGNORED_ON_WIRELESS_CHARGER); @@ -373,17 +355,21 @@ public class VibrationSettingsTest { @Test public void shouldIgnoreVibration_receivesNonWirelessChargingIntent_allowsAnyUsage() { - Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS); - setBatteryReceiverRegistrationResult(wirelessChargingIntent); setIgnoreVibrationsOnWirelessCharger(true); createSystemReadyVibrationSettings(); + + Intent wirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_WIRELESS); + mVibrationSettings.mBatteryBroadcastReceiver.onReceive( + mContextSpy, wirelessChargingIntent); + // Check that initially, all usages are ignored due to the wireless charging. for (int usage : ALL_USAGES) { assertVibrationIgnoredForUsage(usage, Status.IGNORED_ON_WIRELESS_CHARGER); } Intent nonWirelessChargingIntent = getBatteryChangedIntent(BATTERY_PLUGGED_USB); - mRegisteredBatteryBroadcastReceiver.onReceive(mContextSpy, nonWirelessChargingIntent); + mVibrationSettings.mBatteryBroadcastReceiver.onReceive( + mContextSpy, nonWirelessChargingIntent); for (int usage : ALL_USAGES) { assertVibrationNotIgnoredForUsage(usage); @@ -400,7 +386,7 @@ public class VibrationSettingsTest { USAGE_HARDWARE_FEEDBACK )); - mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); + mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); for (int usage : ALL_USAGES) { if (expectedAllowedVibrations.contains(usage)) { @@ -413,7 +399,7 @@ public class VibrationSettingsTest { @Test public void shouldIgnoreVibration_notInBatterySaverMode_allowsAnyUsage() { - mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); + mVibrationSettings.mLowPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); for (int usage : ALL_USAGES) { assertVibrationNotIgnoredForUsage(usage); @@ -596,7 +582,7 @@ public class VibrationSettingsTest { // Testing the broadcast flow manually. when(mAudioManagerMock.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); - mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy, + mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy, new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)); assertVibrationIgnoredForUsage(USAGE_RINGTONE, Status.IGNORED_FOR_RINGER_MODE); @@ -852,16 +838,15 @@ public class VibrationSettingsTest { mVibrationSettings.getCurrentIntensity(USAGE_RINGTONE)); // Test early update of settings based on new user id. - putUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW, - NEW_USER_ID); - mVibrationSettings.mUserSwitchObserver.onUserSwitching(NEW_USER_ID); + putUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW, USER_ID); + mVibrationSettings.mUserSwitchObserver.onUserSwitching(USER_ID); assertEquals(VIBRATION_INTENSITY_LOW, mVibrationSettings.getCurrentIntensity(USAGE_RINGTONE)); // Test later update of settings for UserHandle.USER_CURRENT. putUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_LOW, UserHandle.USER_CURRENT); - mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(NEW_USER_ID); + mVibrationSettings.mUserSwitchObserver.onUserSwitchComplete(USER_ID); assertEquals(VIBRATION_INTENSITY_LOW, mVibrationSettings.getCurrentIntensity(USAGE_RINGTONE)); } @@ -1009,7 +994,7 @@ public class VibrationSettingsTest { private void setRingerMode(int ringerMode) { when(mAudioManagerMock.getRingerModeInternal()).thenReturn(ringerMode); // Mock AudioManager broadcast of internal ringer mode change. - mVibrationSettings.mSettingChangeReceiver.onReceive(mContextSpy, + mVibrationSettings.mRingerModeBroadcastReceiver.onReceive(mContextSpy, new Intent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)); } @@ -1024,14 +1009,6 @@ public class VibrationSettingsTest { return new CallerInfo(attrs, uid, VIRTUAL_DEVICE_ID, opPkg, null); } - private void setBatteryReceiverRegistrationResult(Intent result) { - doAnswer(invocation -> { - mRegisteredBatteryBroadcastReceiver = invocation.getArgument(0); - return result; - }).when(mContextSpy).registerReceiver(any(BroadcastReceiver.class), - argThat(filter -> filter.matchAction(Intent.ACTION_BATTERY_CHANGED)), anyInt()); - } - private Intent getBatteryChangedIntent(int extraPluggedValue) { Intent batteryIntent = new Intent(Intent.ACTION_BATTERY_CHANGED); batteryIntent.putExtra(EXTRA_PLUGGED, extraPluggedValue); diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java index 31cc50f18299..e83a4b22d9bf 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java @@ -29,13 +29,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -96,8 +94,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import java.util.function.BooleanSupplier; import java.util.stream.Collectors; @@ -187,11 +183,11 @@ public class VibrationThreadTest { mVibratorProviders.clear(); CombinedVibration effect = CombinedVibration.createParallel( VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED); + verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED); } @Test @@ -200,11 +196,11 @@ public class VibrationThreadTest { .addNext(2, VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) .addNext(3, VibrationEffect.get(VibrationEffect.EFFECT_TICK)) .combine(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED); + verify(mControllerCallbacks, never()).onComplete(anyInt(), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED); } @Test @@ -212,34 +208,34 @@ public class VibrationThreadTest { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); VibrationEffect effect = VibrationEffect.createOneShot(10, 100); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList(expectedOneShot(10)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()); } @Test public void vibrate_oneShotWithoutAmplitudeControl_runsVibrationWithDefaultAmplitude() { VibrationEffect effect = VibrationEffect.createOneShot(10, 100); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList(expectedOneShot(10)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty()); } @@ -249,17 +245,17 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.createWaveform( new long[]{5, 5, 5}, new int[]{1, 2, 3}, -1); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(15L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList(expectedOneShot(15)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(1, 2, 3), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()); } @@ -277,13 +273,13 @@ public class VibrationThreadTest { mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.5f); CompletableFuture<Void> mRequestVibrationParamsFuture = CompletableFuture.completedFuture( null); - long vibrationId = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture, + HalVibration vibration = startThreadAndDispatcher(effect, mRequestVibrationParamsFuture, USAGE_RINGTONE); waitForCompletion(); verify(mStatsLoggerMock, never()).logVibrationParamRequestTimeout(UID); assertEquals(Arrays.asList(expectedOneShot(15)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes(); for (int i = 0; i < amplitudes.size(); i++) { assertTrue(amplitudes.get(i) < 1 / 255f); @@ -301,12 +297,13 @@ public class VibrationThreadTest { new long[]{5, 5, 5}, new int[]{1, 1, 1}, -1); CompletableFuture<Void> neverCompletingFuture = new CompletableFuture<>(); - long vibrationId = startThreadAndDispatcher(effect, neverCompletingFuture, USAGE_RINGTONE); + HalVibration vibration = startThreadAndDispatcher(effect, neverCompletingFuture, + USAGE_RINGTONE); waitForCompletion(); verify(mStatsLoggerMock).logVibrationParamRequestTimeout(UID); assertEquals(Arrays.asList(expectedOneShot(15)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(1, 1, 1), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()); } @@ -319,13 +316,13 @@ public class VibrationThreadTest { int[] amplitudes = new int[]{1, 2, 3}; VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5, 5, 5}, amplitudes, 0); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue( waitUntil(() -> fakeVibrator.getAmplitudes().size() > 2 * amplitudes.length, TEST_TIMEOUT_MILLIS)); // Vibration still running after 2 cycles. - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); assertTrue(mControllers.get(VIBRATOR_ID).isVibrating()); Vibration.EndInfo cancelVibrationInfo = new Vibration.EndInfo(Status.CANCELLED_SUPERSEDED, @@ -333,15 +330,15 @@ public class VibrationThreadTest { /* uid= */ 1, /* deviceId= */ -1, /* opPkg= */ null, /* reason= */ null)); mVibrationConductor.notifyCancelled(cancelVibrationInfo, /* immediate= */ false); waitForCompletion(); - assertFalse(mThread.isRunningVibrationId(vibrationId)); + assertFalse(mThread.isRunningVibrationId(vibration.id)); verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong()); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verifyCallbacksTriggered(vibrationId, cancelVibrationInfo); + verifyCallbacksTriggered(vibration, Status.CANCELLED_SUPERSEDED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); List<Float> playedAmplitudes = fakeVibrator.getAmplitudes(); - assertFalse(fakeVibrator.getEffectSegments(vibrationId).isEmpty()); + assertFalse(fakeVibrator.getEffectSegments(vibration.id).isEmpty()); assertFalse(playedAmplitudes.isEmpty()); for (int i = 0; i < playedAmplitudes.size(); i++) { @@ -358,17 +355,17 @@ public class VibrationThreadTest { int[] amplitudes = new int[]{1, 2, 3}; VibrationEffect effect = VibrationEffect.createWaveform( new long[]{1, 10, 100}, amplitudes, 0); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> !fakeVibrator.getAmplitudes().isEmpty(), TEST_TIMEOUT_MILLIS)); mVibrationConductor.notifyCancelled( new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false); waitForCompletion(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList(expectedOneShot(5000)), - fakeVibrator.getEffectSegments(vibrationId)); + fakeVibrator.getEffectSegments(vibration.id)); } @Test @@ -377,16 +374,16 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 100, 50, 100, 0, 0, 0, 50}, /* repeat= */ -1); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(300L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse(); - assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)) + assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)) .isEqualTo(expectedOneShots(100L, 150L)); } @@ -398,16 +395,16 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 100, 0, 50, 50, 0, 100, 50}, amplitudes, /* repeat= */ -1); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(350L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse(); - assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)) + assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)) .isEqualTo(expectedOneShots(200L, 50L)); } @@ -420,7 +417,7 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{0, 200, 50, 100, 0, 50, 50, 100}, /* repeat= */ 0); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); // We are expect this test to repeat the vibration effect twice, which would result in 5 // segments being played: // 200ms ON @@ -428,16 +425,17 @@ public class VibrationThreadTest { // 300ms ON (100ms + 200ms looping to the start and skipping first 0ms) // 150ms ON (100ms + 50ms, skips 0ms) // 300ms ON (100ms + 200ms looping to the start and skipping first 0ms) - assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() >= 5, + assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibration.id).size() >= 5, 5000L + TEST_TIMEOUT_MILLIS)); mVibrationConductor.notifyCancelled( new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false); waitForCompletion(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER); assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse(); - assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).subList(0, 5)) + assertThat( + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).subList(0, 5)) .isEqualTo(expectedOneShots(200L, 150L, 300L, 150L, 300L)); } @@ -458,18 +456,18 @@ public class VibrationThreadTest { VibrationEffect repeatingEffect = VibrationEffect.startComposition() .repeatEffectIndefinitely(effect) .compose(); - long vibrationId = startThreadAndDispatcher(repeatingEffect); + HalVibration vibration = startThreadAndDispatcher(repeatingEffect); - assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(), + assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(), TEST_TIMEOUT_MILLIS)); mVibrationConductor.notifyCancelled( new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false); waitForCompletion(); // PWLE size max was used to generate a single vibrate call with 10 segments. - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); - assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size()); + assertEquals(10, fakeVibrator.getEffectSegments(vibration.id).size()); } @Test @@ -487,18 +485,18 @@ public class VibrationThreadTest { VibrationEffect repeatingEffect = VibrationEffect.startComposition() .repeatEffectIndefinitely(effect) .compose(); - long vibrationId = startThreadAndDispatcher(repeatingEffect); + HalVibration vibration = startThreadAndDispatcher(repeatingEffect); - assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(), + assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(), TEST_TIMEOUT_MILLIS)); mVibrationConductor.notifyCancelled( new Vibration.EndInfo(Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ false); waitForCompletion(); // Composition size max was used to generate a single vibrate call with 10 primitives. - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); - assertEquals(10, fakeVibrator.getEffectSegments(vibrationId).size()); + assertEquals(10, fakeVibrator.getEffectSegments(vibration.id).size()); } @Test @@ -510,17 +508,17 @@ public class VibrationThreadTest { int[] amplitudes = new int[]{1, 2, 3}; VibrationEffect effect = VibrationEffect.createWaveform( new long[]{5000, 500, 50}, amplitudes, 0); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> !fakeVibrator.getAmplitudes().isEmpty(), TEST_TIMEOUT_MILLIS)); mVibrationConductor.notifyCancelled( new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false); waitForCompletion(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList(expectedOneShot(5550)), - fakeVibrator.getEffectSegments(vibrationId)); + fakeVibrator.getEffectSegments(vibration.id)); } @LargeTest @@ -534,17 +532,17 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.createWaveform( /* timings= */ new long[]{expectedOnDuration - 100, 50}, /* amplitudes= */ new int[]{1, 2}, /* repeat= */ 0); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); - assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() > 1, + assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibration.id).size() > 1, expectedOnDuration + TEST_TIMEOUT_MILLIS)); mVibrationConductor.notifyCancelled( new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false); waitForCompletion(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); - List<VibrationEffectSegment> effectSegments = fakeVibrator.getEffectSegments(vibrationId); + List<VibrationEffectSegment> effectSegments = fakeVibrator.getEffectSegments(vibration.id); // First time, turn vibrator ON for the expected fixed duration. assertEquals(expectedOnDuration, effectSegments.get(0).getDuration()); // Vibrator turns off in the middle of the second execution of the first step. Expect it to @@ -567,11 +565,11 @@ public class VibrationThreadTest { .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100) .compose(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(), TEST_TIMEOUT_MILLIS)); - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. @@ -584,7 +582,7 @@ public class VibrationThreadTest { waitForCompletion(/* timeout= */ 50); cancellingThread.join(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SETTINGS_UPDATE); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SETTINGS_UPDATE); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); } @@ -597,11 +595,11 @@ public class VibrationThreadTest { mVibratorProviders.get(VIBRATOR_ID).setVendorEffectDuration(10 * TEST_TIMEOUT_MILLIS); VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData()); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(), TEST_TIMEOUT_MILLIS)); - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. @@ -614,7 +612,7 @@ public class VibrationThreadTest { waitForCompletion(/* timeout= */ 50); cancellingThread.join(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SETTINGS_UPDATE); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SETTINGS_UPDATE); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); } @@ -624,11 +622,11 @@ public class VibrationThreadTest { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS); VibrationEffect effect = VibrationEffect.createWaveform(new long[]{100}, new int[]{100}, 0); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(), TEST_TIMEOUT_MILLIS)); - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. @@ -641,7 +639,7 @@ public class VibrationThreadTest { waitForCompletion(/* timeout= */ 50); cancellingThread.join(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); } @@ -650,17 +648,17 @@ public class VibrationThreadTest { mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_THUD); VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_THUD); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_THUD)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); } @Test @@ -671,31 +669,31 @@ public class VibrationThreadTest { HalVibration vibration = createVibration(CombinedVibration.createParallel( VibrationEffect.get(VibrationEffect.EFFECT_CLICK))); vibration.addFallback(VibrationEffect.EFFECT_CLICK, fallback); - long vibrationId = startThreadAndDispatcher(vibration); + startThreadAndDispatcher(vibration); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList(expectedOneShot(10)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()); } @Test public void vibrate_singleVibratorPrebakedAndUnsupportedEffect_ignoresVibration() { VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L)); verify(mManagerHooks, never()).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED); - assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty()); + verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED); + assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty()); } @Test @@ -704,17 +702,17 @@ public class VibrationThreadTest { mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData()); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(PerformVendorEffectVibratorStep.VENDOR_EFFECT_MAX_DURATION_MS)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse(); - assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId)) + assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibration.id)) .containsExactly(effect) .inOrder(); } @@ -730,18 +728,18 @@ public class VibrationThreadTest { .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) .compose(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(40L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0), expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 0)), - fakeVibrator.getEffectSegments(vibrationId)); + fakeVibrator.getEffectSegments(vibration.id)); } @Test @@ -749,14 +747,14 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f) .compose(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(0L)); verify(mManagerHooks, never()).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.IGNORED_UNSUPPORTED); - assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty()); + verify(mControllerCallbacks, never()).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.IGNORED_UNSUPPORTED); + assertTrue(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty()); } @Test @@ -774,13 +772,13 @@ public class VibrationThreadTest { .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f) .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f) .compose(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verifyCallbacksTriggered(vibration, Status.FINISHED); // Vibrator compose called twice. - verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - assertEquals(3, fakeVibrator.getEffectSegments(vibrationId).size()); + verify(mControllerCallbacks, times(2)).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + assertEquals(3, fakeVibrator.getEffectSegments(vibration.id).size()); } @Test @@ -810,14 +808,14 @@ public class VibrationThreadTest { .build()) .addEffect(VibrationEffect.get(VibrationEffect.EFFECT_CLICK)) .compose(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); // Use first duration the vibrator is turned on since we cannot estimate the clicks. verify(mManagerHooks).noteVibratorOn(eq(UID), eq(10L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks, times(5)).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( expectedOneShot(10), @@ -829,7 +827,7 @@ public class VibrationThreadTest { expectedRamp(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.7f, /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 120, /* duration= */ 20), expectedPrebaked(VibrationEffect.EFFECT_CLICK)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(100), mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()); } @@ -851,18 +849,18 @@ public class VibrationThreadTest { .compose(); HalVibration vibration = createVibration(CombinedVibration.createParallel(effect)); vibration.addFallback(VibrationEffect.EFFECT_TICK, fallback); - long vibrationId = startThreadAndDispatcher(vibration); + startThreadAndDispatcher(vibration); waitForCompletion(); // Use first duration the vibrator is turned on since we cannot estimate the clicks. verify(mManagerHooks).noteVibratorOn(eq(UID), anyLong()); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks, times(4)).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); List<VibrationEffectSegment> segments = - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id); assertTrue("Wrong segments: " + segments, segments.size() >= 4); assertTrue(segments.get(0) instanceof PrebakedSegment); assertTrue(segments.get(1) instanceof PrimitiveSegment); @@ -891,13 +889,13 @@ public class VibrationThreadTest { .addSustain(Duration.ofMillis(30)) .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200)) .build(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( expectedRamp(/* amplitude= */ 1, /* frequencyHz= */ 150, /* duration= */ 10), @@ -907,8 +905,8 @@ public class VibrationThreadTest { expectedRamp(/* startAmplitude= */ 0.5f, /* endAmplitude= */ 0.6f, /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 200, /* duration= */ 40)), - fakeVibrator.getEffectSegments(vibrationId)); - assertEquals(Arrays.asList(Braking.CLAB), fakeVibrator.getBraking(vibrationId)); + fakeVibrator.getEffectSegments(vibration.id)); + assertEquals(Arrays.asList(Braking.CLAB), fakeVibrator.getBraking(vibration.id)); } @Test @@ -932,15 +930,15 @@ public class VibrationThreadTest { .addTransition(Duration.ofMillis(40), targetAmplitude(0.7f), targetFrequency(200)) .addTransition(Duration.ofMillis(40), targetAmplitude(0.6f), targetFrequency(200)) .build(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verifyCallbacksTriggered(vibration, Status.FINISHED); // Vibrator compose called 3 times with 2 segments instead of 2 times with 3 segments. // Using best split points instead of max-packing PWLEs. - verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - assertEquals(6, fakeVibrator.getEffectSegments(vibrationId).size()); + verify(mControllerCallbacks, times(3)).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + assertEquals(6, fakeVibrator.getEffectSegments(vibration.id).size()); } @Test @@ -949,28 +947,28 @@ public class VibrationThreadTest { fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> fakeVibrator.getAmplitudes().size() > 2, TEST_TIMEOUT_MILLIS)); // Vibration still running after 2 cycles. - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); assertTrue(mControllers.get(VIBRATOR_ID).isVibrating()); mVibrationConductor.binderDied(); waitForCompletion(); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BINDER_DIED); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BINDER_DIED); } @Test public void vibrate_singleVibrator_skipsSyncedCallbacks() { mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); - long vibrationId = startThreadAndDispatcher(VibrationEffect.createOneShot(10, 100)); + HalVibration vibration = startThreadAndDispatcher(VibrationEffect.createOneShot(10, 100)); waitForCompletion(); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verifyCallbacksTriggered(vibration, Status.FINISHED); verify(mManagerHooks, never()).prepareSyncedVibration(anyLong(), any()); verify(mManagerHooks, never()).triggerSyncedVibration(anyLong()); verify(mManagerHooks, never()).cancelSyncedVibration(); @@ -984,18 +982,18 @@ public class VibrationThreadTest { .addVibrator(VIBRATOR_ID, VibrationEffect.get(VibrationEffect.EFFECT_TICK)) .addVibrator(2, VibrationEffect.get(VibrationEffect.EFFECT_TICK)) .combine(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verify(mControllerCallbacks, never()).onComplete(eq(2), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_TICK)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); } @Test @@ -1007,26 +1005,26 @@ public class VibrationThreadTest { CombinedVibration effect = CombinedVibration.createParallel( VibrationEffect.get(VibrationEffect.EFFECT_CLICK)); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId)); - verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId)); - verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); assertFalse(mControllers.get(3).isVibrating()); VibrationEffectSegment expected = expectedPrebaked(VibrationEffect.EFFECT_CLICK); assertEquals(Arrays.asList(expected), - mVibratorProviders.get(1).getEffectSegments(vibrationId)); + mVibratorProviders.get(1).getEffectSegments(vibration.id)); assertEquals(Arrays.asList(expected), - mVibratorProviders.get(2).getEffectSegments(vibrationId)); + mVibratorProviders.get(2).getEffectSegments(vibration.id)); assertEquals(Arrays.asList(expected), - mVibratorProviders.get(3).getEffectSegments(vibrationId)); + mVibratorProviders.get(3).getEffectSegments(vibration.id)); } @Test @@ -1049,32 +1047,32 @@ public class VibrationThreadTest { new long[]{10, 10}, new int[]{1, 2}, -1)) .addVibrator(4, composed) .combine(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId)); - verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId)); - verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId)); - verify(mControllerCallbacks).onComplete(eq(4), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(4), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); assertFalse(mControllers.get(3).isVibrating()); assertFalse(mControllers.get(4).isVibrating()); assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)), - mVibratorProviders.get(1).getEffectSegments(vibrationId)); + mVibratorProviders.get(1).getEffectSegments(vibration.id)); assertEquals(Arrays.asList(expectedOneShot(10)), - mVibratorProviders.get(2).getEffectSegments(vibrationId)); + mVibratorProviders.get(2).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(100), mVibratorProviders.get(2).getAmplitudes()); assertEquals(Arrays.asList(expectedOneShot(20)), - mVibratorProviders.get(3).getEffectSegments(vibrationId)); + mVibratorProviders.get(3).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(1, 2), mVibratorProviders.get(3).getAmplitudes()); assertEquals(Arrays.asList( expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)), - mVibratorProviders.get(4).getEffectSegments(vibrationId)); + mVibratorProviders.get(4).getEffectSegments(vibration.id)); } @Test @@ -1094,13 +1092,13 @@ public class VibrationThreadTest { .addNext(1, VibrationEffect.createOneShot(10, 100), /* delay= */ 50) .addNext(2, composed, /* delay= */ 50) .combine(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); InOrder controllerVerifier = inOrder(mControllerCallbacks); - controllerVerifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId)); - controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId)); - controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId)); + controllerVerifier.verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id)); + controllerVerifier.verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id)); + controllerVerifier.verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id)); InOrder batteryVerifier = inOrder(mManagerHooks); batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); @@ -1110,19 +1108,19 @@ public class VibrationThreadTest { batteryVerifier.verify(mManagerHooks).noteVibratorOn(eq(UID), eq(20L)); batteryVerifier.verify(mManagerHooks).noteVibratorOff(eq(UID)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); assertFalse(mControllers.get(3).isVibrating()); assertEquals(Arrays.asList(expectedOneShot(10)), - mVibratorProviders.get(1).getEffectSegments(vibrationId)); + mVibratorProviders.get(1).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(100), mVibratorProviders.get(1).getAmplitudes()); assertEquals(Arrays.asList( expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)), - mVibratorProviders.get(2).getEffectSegments(vibrationId)); + mVibratorProviders.get(2).getEffectSegments(vibration.id)); assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)), - mVibratorProviders.get(3).getEffectSegments(vibrationId)); + mVibratorProviders.get(3).getEffectSegments(vibration.id)); } @Test @@ -1143,30 +1141,29 @@ public class VibrationThreadTest { CombinedVibration effect = CombinedVibration.createParallel(composed); // We create the HalVibration here to obtain the vibration id and use it to mock the // required response when calling triggerSyncedVibration. - HalVibration halVibration = createVibration(effect); - long vibrationId = halVibration.id; - when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true); - startThreadAndDispatcher(halVibration); + HalVibration vibration = createVibration(effect); + when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(true); + startThreadAndDispatcher(vibration); assertTrue(waitUntil( - () -> !mVibratorProviders.get(1).getEffectSegments(vibrationId).isEmpty() - && !mVibratorProviders.get(2).getEffectSegments(vibrationId).isEmpty(), + () -> !mVibratorProviders.get(1).getEffectSegments(vibration.id).isEmpty() + && !mVibratorProviders.get(2).getEffectSegments(vibration.id).isEmpty(), TEST_TIMEOUT_MILLIS)); mVibrationConductor.notifySyncedVibrationComplete(); waitForCompletion(); long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_COMPOSE; verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds)); - verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId)); + verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id)); verify(mManagerHooks, never()).cancelSyncedVibration(); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verifyCallbacksTriggered(vibration, Status.FINISHED); VibrationEffectSegment expected = expectedPrimitive( VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 100); assertEquals(Arrays.asList(expected), - mVibratorProviders.get(1).getEffectSegments(vibrationId)); + mVibratorProviders.get(1).getEffectSegments(vibration.id)); assertEquals(Arrays.asList(expected), - mVibratorProviders.get(2).getEffectSegments(vibrationId)); + mVibratorProviders.get(2).getEffectSegments(vibration.id)); } @Test @@ -1190,10 +1187,9 @@ public class VibrationThreadTest { .combine(); // We create the HalVibration here to obtain the vibration id and use it to mock the // required response when calling triggerSyncedVibration. - HalVibration halVibration = createVibration(effect); - long vibrationId = halVibration.id; - when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(true); - startThreadAndDispatcher(halVibration); + HalVibration vibration = createVibration(effect); + when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(true); + startThreadAndDispatcher(vibration); waitForCompletion(); long expectedCap = IVibratorManager.CAP_SYNC @@ -1204,9 +1200,9 @@ public class VibrationThreadTest { | IVibratorManager.CAP_MIXED_TRIGGER_PERFORM | IVibratorManager.CAP_MIXED_TRIGGER_COMPOSE; verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds)); - verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId)); + verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id)); verify(mManagerHooks, never()).cancelSyncedVibration(); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verifyCallbacksTriggered(vibration, Status.FINISHED); } @Test @@ -1221,19 +1217,19 @@ public class VibrationThreadTest { .addVibrator(1, VibrationEffect.createOneShot(10, 100)) .addVibrator(2, VibrationEffect.createWaveform(new long[]{5}, new int[]{200}, -1)) .combine(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); long expectedCap = IVibratorManager.CAP_SYNC | IVibratorManager.CAP_PREPARE_ON; verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds)); - verify(mManagerHooks, never()).triggerSyncedVibration(eq(vibrationId)); + verify(mManagerHooks, never()).triggerSyncedVibration(eq(vibration.id)); verify(mManagerHooks, never()).cancelSyncedVibration(); assertEquals(Arrays.asList(expectedOneShot(10)), - mVibratorProviders.get(1).getEffectSegments(vibrationId)); + mVibratorProviders.get(1).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(100), mVibratorProviders.get(1).getAmplitudes()); assertEquals(Arrays.asList(expectedOneShot(5)), - mVibratorProviders.get(2).getEffectSegments(vibrationId)); + mVibratorProviders.get(2).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(200), mVibratorProviders.get(2).getAmplitudes()); } @@ -1250,10 +1246,9 @@ public class VibrationThreadTest { .combine(); // We create the HalVibration here to obtain the vibration id and use it to mock the // required response when calling triggerSyncedVibration. - HalVibration halVibration = createVibration(effect); - long vibrationId = halVibration.id; - when(mManagerHooks.triggerSyncedVibration(eq(vibrationId))).thenReturn(false); - startThreadAndDispatcher(halVibration); + HalVibration vibration = createVibration(effect); + when(mManagerHooks.triggerSyncedVibration(eq(vibration.id))).thenReturn(false); + startThreadAndDispatcher(vibration); waitForCompletion(); long expectedCap = IVibratorManager.CAP_SYNC @@ -1262,7 +1257,7 @@ public class VibrationThreadTest { | IVibratorManager.CAP_MIXED_TRIGGER_ON | IVibratorManager.CAP_MIXED_TRIGGER_PERFORM; verify(mManagerHooks).prepareSyncedVibration(eq(expectedCap), eq(vibratorIds)); - verify(mManagerHooks).triggerSyncedVibration(eq(vibrationId)); + verify(mManagerHooks).triggerSyncedVibration(eq(vibration.id)); verify(mManagerHooks).cancelSyncedVibration(); assertTrue(mVibratorProviders.get(1).getAmplitudes().isEmpty()); } @@ -1282,7 +1277,7 @@ public class VibrationThreadTest { .addVibrator(3, VibrationEffect.createWaveform( new long[]{60}, new int[]{6}, -1)) .combine(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); // All vibrators are turned on in parallel. assertTrue(waitUntil( @@ -1295,20 +1290,20 @@ public class VibrationThreadTest { verify(mManagerHooks).noteVibratorOn(eq(UID), eq(80L)); verify(mManagerHooks).noteVibratorOff(eq(UID)); - verify(mControllerCallbacks).onComplete(eq(1), eq(vibrationId)); - verify(mControllerCallbacks).onComplete(eq(2), eq(vibrationId)); - verify(mControllerCallbacks).onComplete(eq(3), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(1), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(2), eq(vibration.id)); + verify(mControllerCallbacks).onComplete(eq(3), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); assertFalse(mControllers.get(3).isVibrating()); assertEquals(Arrays.asList(expectedOneShot(25)), - mVibratorProviders.get(1).getEffectSegments(vibrationId)); + mVibratorProviders.get(1).getEffectSegments(vibration.id)); assertEquals(Arrays.asList(expectedOneShot(80)), - mVibratorProviders.get(2).getEffectSegments(vibrationId)); + mVibratorProviders.get(2).getEffectSegments(vibration.id)); assertEquals(Arrays.asList(expectedOneShot(60)), - mVibratorProviders.get(3).getEffectSegments(vibrationId)); + mVibratorProviders.get(3).getEffectSegments(vibration.id)); assertEquals(expectedAmplitudes(1, 2, 3), mVibratorProviders.get(1).getAmplitudes()); assertEquals(expectedAmplitudes(4, 5), mVibratorProviders.get(2).getAmplitudes()); assertEquals(expectedAmplitudes(6), mVibratorProviders.get(3).getAmplitudes()); @@ -1327,17 +1322,11 @@ public class VibrationThreadTest { CombinedVibration.createParallel( VibrationEffect.createOneShot( expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE))); - CountDownLatch vibrationCompleteLatch = new CountDownLatch(1); - doAnswer(unused -> { - vibrationCompleteLatch.countDown(); - return null; - }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any()); startThreadAndDispatcher(vibration); long startTime = SystemClock.elapsedRealtime(); - assertTrue(vibrationCompleteLatch.await(expectedDuration + TEST_TIMEOUT_MILLIS, - TimeUnit.MILLISECONDS)); + vibration.waitForEnd(); long vibrationEndTime = SystemClock.elapsedRealtime(); waitForCompletion(rampDownDuration + TEST_TIMEOUT_MILLIS); @@ -1363,17 +1352,11 @@ public class VibrationThreadTest { CombinedVibration.createParallel( VibrationEffect.createOneShot( expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE))); - CountDownLatch vibrationCompleteLatch = new CountDownLatch(1); - doAnswer(unused -> { - vibrationCompleteLatch.countDown(); - return null; - }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any()); startThreadAndDispatcher(vibration); long startTime = SystemClock.elapsedRealtime(); - assertTrue(vibrationCompleteLatch.await(callbackDelay + TEST_TIMEOUT_MILLIS, - TimeUnit.MILLISECONDS)); + vibration.waitForEnd(); long vibrationEndTime = SystemClock.elapsedRealtime(); waitForCompletion(TEST_TIMEOUT_MILLIS); @@ -1397,17 +1380,11 @@ public class VibrationThreadTest { CombinedVibration.createParallel( VibrationEffect.createOneShot( expectedDuration, VibrationEffect.DEFAULT_AMPLITUDE))); - CountDownLatch vibrationCompleteLatch = new CountDownLatch(1); - doAnswer(unused -> { - vibrationCompleteLatch.countDown(); - return null; - }).when(mManagerHooks).onVibrationCompleted(eq(vibration.id), any()); startThreadAndDispatcher(vibration); long startTime = SystemClock.elapsedRealtime(); - assertTrue(vibrationCompleteLatch.await(callbackTimeout + TEST_TIMEOUT_MILLIS, - TimeUnit.MILLISECONDS)); + vibration.waitForEnd(); long vibrationEndTime = SystemClock.elapsedRealtime(); waitForCompletion(callbackDelay + TEST_TIMEOUT_MILLIS); @@ -1461,11 +1438,11 @@ public class VibrationThreadTest { fakeVibrator.setOnLatency(latency); VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); - assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibrationId).isEmpty(), + assertTrue(waitUntil(() -> !fakeVibrator.getEffectSegments(vibration.id).isEmpty(), TEST_TIMEOUT_MILLIS)); - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(cancellingThread). @@ -1480,7 +1457,7 @@ public class VibrationThreadTest { // After the vibrator call ends the vibration is cancelled and the vibrator is turned off. waitForCompletion(/* timeout= */ latency + TEST_TIMEOUT_MILLIS); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); } @@ -1500,10 +1477,10 @@ public class VibrationThreadTest { .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 100) .compose()) .combine(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS)); - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. @@ -1516,7 +1493,7 @@ public class VibrationThreadTest { waitForCompletion(/* timeout= */ 50); cancellingThread.join(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); } @@ -1534,10 +1511,10 @@ public class VibrationThreadTest { .addVibrator(1, VibrationEffect.createVendorEffect(createTestVendorData())) .addVibrator(2, VibrationEffect.createVendorEffect(createTestVendorData())) .combine(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS)); - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. @@ -1550,7 +1527,7 @@ public class VibrationThreadTest { waitForCompletion(/* timeout= */ 50); cancellingThread.join(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); } @@ -1566,12 +1543,12 @@ public class VibrationThreadTest { new long[]{100, 100}, new int[]{1, 2}, 0)) .addVibrator(2, VibrationEffect.createOneShot(100, 100)) .combine(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> mControllers.get(1).isVibrating() && mControllers.get(2).isVibrating(), TEST_TIMEOUT_MILLIS)); - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); // Run cancel in a separate thread so if VibrationThread.cancel blocks then this test should // fail at waitForCompletion(vibrationThread) if the vibration not cancelled immediately. @@ -1584,7 +1561,7 @@ public class VibrationThreadTest { waitForCompletion(/* timeout= */ 50); cancellingThread.join(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_SCREEN_OFF); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_SCREEN_OFF); assertFalse(mControllers.get(1).isVibrating()); assertFalse(mControllers.get(2).isVibrating()); } @@ -1592,19 +1569,17 @@ public class VibrationThreadTest { @Test public void vibrate_binderDied_cancelsVibration() throws Exception { VibrationEffect effect = VibrationEffect.createWaveform(new long[]{5}, new int[]{100}, 0); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(), TEST_TIMEOUT_MILLIS)); - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); mVibrationConductor.binderDied(); waitForCompletion(); - verify(mVibrationToken).linkToDeath(same(mVibrationConductor), eq(0)); - verify(mVibrationToken).unlinkToDeath(same(mVibrationConductor), eq(0)); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BINDER_DIED); - assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).isEmpty()); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BINDER_DIED); + assertFalse(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id).isEmpty()); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); } @@ -1615,15 +1590,15 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.createWaveform( new long[]{5, 5, 5}, new int[]{60, 120, 240}, -1); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); // Duration extended for 5 + 5 + 5 + 15. assertEquals(Arrays.asList(expectedOneShot(30)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes(); assertTrue(amplitudes.size() > 3); assertEquals(expectedAmplitudes(60, 120, 240), amplitudes.subList(0, 3)); @@ -1633,24 +1608,24 @@ public class VibrationThreadTest { } @Test - public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds() { + public void vibrate_waveformWithRampDown_triggersCallbackWhenOriginalVibrationEnds() + throws Exception { when(mVibrationConfigMock.getRampDownDurationMs()).thenReturn(10_000); mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); VibrationEffect effect = VibrationEffect.createOneShot(10, 200); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); // Vibration completed but vibrator not yet released. - verify(mManagerHooks, timeout(TEST_TIMEOUT_MILLIS)).onVibrationCompleted(eq(vibrationId), - eq(new Vibration.EndInfo(Status.FINISHED))); + vibration.waitForEnd(); verify(mManagerHooks, never()).onVibrationThreadReleased(anyLong()); // Thread still running ramp down. - assertTrue(mThread.isRunningVibrationId(vibrationId)); + assertTrue(mThread.isRunningVibrationId(vibration.id)); // Duration extended for 10 + 10000. assertEquals(Arrays.asList(expectedOneShot(10_010)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); // Will stop the ramp down right away. mVibrationConductor.notifyCancelled( @@ -1658,9 +1633,8 @@ public class VibrationThreadTest { waitForCompletion(); // Does not cancel already finished vibration, but releases vibrator. - verify(mManagerHooks, never()).onVibrationCompleted(eq(vibrationId), - eq(new Vibration.EndInfo(Status.CANCELLED_BY_SETTINGS_UPDATE))); - verify(mManagerHooks).onVibrationThreadReleased(vibrationId); + assertThat(vibration.getStatus()).isNotEqualTo(Status.CANCELLED_BY_SETTINGS_UPDATE); + verify(mManagerHooks).onVibrationThreadReleased(vibration.id); } @Test @@ -1670,18 +1644,18 @@ public class VibrationThreadTest { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); VibrationEffect effect = VibrationEffect.createOneShot(10_000, 240); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); assertTrue(waitUntil(() -> mControllers.get(VIBRATOR_ID).isVibrating(), TEST_TIMEOUT_MILLIS)); mVibrationConductor.notifyCancelled( new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false); waitForCompletion(); - verifyCallbacksTriggered(vibrationId, Status.CANCELLED_BY_USER); + verifyCallbacksTriggered(vibration, Status.CANCELLED_BY_USER); // Duration extended for 10000 + 15. assertEquals(Arrays.asList(expectedOneShot(10_015)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); List<Float> amplitudes = mVibratorProviders.get(VIBRATOR_ID).getAmplitudes(); assertTrue(amplitudes.size() > 1); for (int i = 1; i < amplitudes.size(); i++) { @@ -1696,14 +1670,14 @@ public class VibrationThreadTest { mVibratorProviders.get(VIBRATOR_ID).setSupportedEffects(VibrationEffect.EFFECT_CLICK); VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty()); } @@ -1714,13 +1688,13 @@ public class VibrationThreadTest { mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); VibrationEffect effect = VibrationEffect.createVendorEffect(createTestVendorData()); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); - assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibrationId)) + assertThat(mVibratorProviders.get(VIBRATOR_ID).getVendorEffects(vibration.id)) .containsExactly(effect) .inOrder(); assertThat(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes()).isEmpty(); @@ -1737,15 +1711,15 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.startComposition() .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK) .compose(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertEquals( Arrays.asList(expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)), - mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId)); + mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibration.id)); assertTrue(mVibratorProviders.get(VIBRATOR_ID).getAmplitudes().isEmpty()); } @@ -1764,14 +1738,14 @@ public class VibrationThreadTest { VibrationEffect effect = VibrationEffect.startWaveform() .addTransition(Duration.ofMillis(1), targetAmplitude(1)) .build(); - long vibrationId = startThreadAndDispatcher(effect); + HalVibration vibration = startThreadAndDispatcher(effect); waitForCompletion(); - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId)); - verifyCallbacksTriggered(vibrationId, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); assertEquals(Arrays.asList(expectedRamp(0, 1, 150, 150, 1)), - fakeVibrator.getEffectSegments(vibrationId)); + fakeVibrator.getEffectSegments(vibration.id)); assertTrue(fakeVibrator.getAmplitudes().isEmpty()); } @@ -1796,69 +1770,68 @@ public class VibrationThreadTest { VibrationEffect effect4 = VibrationEffect.createOneShot(8000, 100); VibrationEffect effect5 = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); - long vibrationId1 = startThreadAndDispatcher(effect1); + HalVibration vibration1 = startThreadAndDispatcher(effect1); waitForCompletion(); - verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibrationId1); - verifyCallbacksTriggered(vibrationId1, Status.FINISHED); - long vibrationId2 = startThreadAndDispatcher(effect2); + HalVibration vibration2 = startThreadAndDispatcher(effect2); // Effect2 won't complete on its own. Cancel it after a couple of repeats. Thread.sleep(150); // More than two TICKs. mVibrationConductor.notifyCancelled( new Vibration.EndInfo(Status.CANCELLED_BY_USER), /* immediate= */ false); waitForCompletion(); - long vibrationId3 = startThreadAndDispatcher(effect3); + HalVibration vibration3 = startThreadAndDispatcher(effect3); waitForCompletion(); // Effect4 is a long oneshot, but it gets cancelled as fast as possible. long start4 = System.currentTimeMillis(); - long vibrationId4 = startThreadAndDispatcher(effect4); + HalVibration vibration4 = startThreadAndDispatcher(effect4); mVibrationConductor.notifyCancelled( new Vibration.EndInfo(Status.CANCELLED_BY_SCREEN_OFF), /* immediate= */ true); waitForCompletion(); long duration4 = System.currentTimeMillis() - start4; // Effect5 is to show that things keep going after the immediate cancel. - long vibrationId5 = startThreadAndDispatcher(effect5); + HalVibration vibration5 = startThreadAndDispatcher(effect5); waitForCompletion(); FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); // Effect1 - verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibrationId1); - verifyCallbacksTriggered(vibrationId1, Status.FINISHED); + verify(mControllerCallbacks).onComplete(VIBRATOR_ID, vibration1.id); + verifyCallbacksTriggered(vibration1, Status.FINISHED); assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)), - fakeVibrator.getEffectSegments(vibrationId1)); + fakeVibrator.getEffectSegments(vibration1.id)); // Effect2: repeating, cancelled. - verify(mControllerCallbacks, atLeast(2)).onComplete(VIBRATOR_ID, vibrationId2); - verifyCallbacksTriggered(vibrationId2, Status.CANCELLED_BY_USER); + verify(mControllerCallbacks, atLeast(2)).onComplete(VIBRATOR_ID, vibration2.id); + verifyCallbacksTriggered(vibration2, Status.CANCELLED_BY_USER); // The exact count of segments might vary, so just check that there's more than 2 and // all elements are the same segment. - List<VibrationEffectSegment> actualSegments2 = fakeVibrator.getEffectSegments(vibrationId2); + List<VibrationEffectSegment> actualSegments2 = + fakeVibrator.getEffectSegments(vibration2.id); assertTrue(actualSegments2.size() + " > 2", actualSegments2.size() > 2); for (VibrationEffectSegment segment : actualSegments2) { assertEquals(expectedPrebaked(VibrationEffect.EFFECT_TICK), segment); } // Effect3 - verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibrationId3)); - verifyCallbacksTriggered(vibrationId3, Status.FINISHED); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration3.id)); + verifyCallbacksTriggered(vibration3, Status.FINISHED); assertEquals(Arrays.asList( expectedPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0)), - fakeVibrator.getEffectSegments(vibrationId3)); + fakeVibrator.getEffectSegments(vibration3.id)); // Effect4: cancelled quickly. - verifyCallbacksTriggered(vibrationId4, Status.CANCELLED_BY_SCREEN_OFF); + verifyCallbacksTriggered(vibration4, Status.CANCELLED_BY_SCREEN_OFF); assertTrue("Tested duration=" + duration4, duration4 < 2000); // Effect5: played normally after effect4, which may or may not have played. assertEquals(Arrays.asList(expectedPrebaked(VibrationEffect.EFFECT_CLICK)), - fakeVibrator.getEffectSegments(vibrationId5)); + fakeVibrator.getEffectSegments(vibration5.id)); } private void mockVibrators(int... vibratorIds) { @@ -1875,19 +1848,19 @@ public class VibrationThreadTest { mVibrationSettings.mSettingObserver.onChange(false); } - private long startThreadAndDispatcher(VibrationEffect effect) { + private HalVibration startThreadAndDispatcher(VibrationEffect effect) { return startThreadAndDispatcher(CombinedVibration.createParallel(effect)); } - private long startThreadAndDispatcher(CombinedVibration effect) { + private HalVibration startThreadAndDispatcher(CombinedVibration effect) { return startThreadAndDispatcher(createVibration(effect)); } - private long startThreadAndDispatcher(HalVibration vib) { + private HalVibration startThreadAndDispatcher(HalVibration vib) { return startThreadAndDispatcher(vib, /* requestVibrationParamsFuture= */ null); } - private long startThreadAndDispatcher(VibrationEffect effect, + private HalVibration startThreadAndDispatcher(VibrationEffect effect, CompletableFuture<Void> requestVibrationParamsFuture, int usage) { VibrationAttributes attrs = new VibrationAttributes.Builder() .setUsage(usage) @@ -1898,14 +1871,14 @@ public class VibrationThreadTest { return startThreadAndDispatcher(vib, requestVibrationParamsFuture); } - private long startThreadAndDispatcher(HalVibration vib, + private HalVibration startThreadAndDispatcher(HalVibration vib, CompletableFuture<Void> requestVibrationParamsFuture) { mControllers = createVibratorControllers(); DeviceAdapter deviceAdapter = new DeviceAdapter(mVibrationSettings, mControllers); mVibrationConductor = new VibrationStepConductor(vib, mVibrationSettings, deviceAdapter, mVibrationScaler, mStatsLoggerMock, requestVibrationParamsFuture, mManagerHooks); assertTrue(mThread.runVibrationOnVibrationThread(mVibrationConductor)); - return mVibrationConductor.getVibration().id; + return mVibrationConductor.getVibration(); } private boolean waitUntil(BooleanSupplier predicate, long timeout) @@ -1994,13 +1967,9 @@ public class VibrationThreadTest { .collect(Collectors.toList()); } - private void verifyCallbacksTriggered(long vibrationId, Status expectedStatus) { - verifyCallbacksTriggered(vibrationId, new Vibration.EndInfo(expectedStatus)); - } - - private void verifyCallbacksTriggered(long vibrationId, Vibration.EndInfo expectedEndInfo) { - verify(mManagerHooks).onVibrationCompleted(eq(vibrationId), eq(expectedEndInfo)); - verify(mManagerHooks).onVibrationThreadReleased(vibrationId); + private void verifyCallbacksTriggered(HalVibration vibration, Status expectedStatus) { + assertThat(vibration.getStatus()).isEqualTo(expectedStatus); + verify(mManagerHooks).onVibrationThreadReleased(vibration.id); } private static final class TestLooperAutoDispatcher extends Thread { diff --git a/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java b/services/tests/wmtests/src/com/android/server/policy/CombinationKeyTests.java index 1c331166d317..6f9c8904ca32 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 android.view.KeyEvent.KEYCODE_VOLUME_UP; 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 @@ import org.junit.runner.RunWith; */ @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 487390d4411d..8b5f68a1e974 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 @@ public class KeyCombinationManagerTests { 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 @@ public class KeyCombinationManagerTests { 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 @@ public class KeyCombinationManagerTests { }; @Override - void execute() { + public void execute() { mHandler.postDelayed(mAction, SCHEDULE_TIME); } @Override - void cancel() { + public void cancel() { mHandler.removeCallbacks(mAction); } }); @@ -235,12 +235,12 @@ public class KeyCombinationManagerTests { 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 3d978e424375..cdb45423c11a 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.DOUBLE_TAP_HOME_RECEN 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,7 +59,117 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { 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[][]{ + {"HOME key -> Open Home", new int[]{KeyEvent.KEYCODE_HOME}, + KeyGestureEvent.KEY_GESTURE_TYPE_HOME, + KeyEvent.KEYCODE_HOME, 0}, + {"BACK key -> Go back", new int[]{KeyEvent.KEYCODE_BACK}, + KeyGestureEvent.KEY_GESTURE_TYPE_BACK, + KeyEvent.KEYCODE_BACK, 0}, + {"VOLUME_UP key -> Increase Volume", new int[]{KeyEvent.KEYCODE_VOLUME_UP}, + KeyGestureEvent.KEY_GESTURE_TYPE_VOLUME_UP, + KeyEvent.KEYCODE_VOLUME_UP, 0}, + {"VOLUME_DOWN key -> Decrease Volume", new int[]{KeyEvent.KEYCODE_VOLUME_DOWN}, + KeyGestureEvent.KEY_GESTURE_TYPE_VOLUME_DOWN, + KeyEvent.KEYCODE_VOLUME_DOWN, 0}, + {"VOLUME_MUTE key -> Mute Volume", new int[]{KeyEvent.KEYCODE_VOLUME_MUTE}, + KeyGestureEvent.KEY_GESTURE_TYPE_VOLUME_MUTE, + KeyEvent.KEYCODE_VOLUME_MUTE, 0}, + {"MUTE key -> Mute System Microphone", new int[]{KeyEvent.KEYCODE_MUTE}, + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_MUTE, KeyEvent.KEYCODE_MUTE, + 0}, + {"POWER key -> Toggle Power", new int[]{KeyEvent.KEYCODE_POWER}, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_POWER, KeyEvent.KEYCODE_POWER, + 0}, + {"TV_POWER key -> Toggle Power", new int[]{KeyEvent.KEYCODE_TV_POWER}, + KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_POWER, + KeyEvent.KEYCODE_TV_POWER, 0}, + {"SYSTEM_NAVIGATION_DOWN key -> System Navigation", + new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN}, + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_NAVIGATION, + KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN, + 0}, + {"SYSTEM_NAVIGATION_UP key -> System Navigation", + new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP}, + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_NAVIGATION, + KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP, + 0}, + {"SYSTEM_NAVIGATION_LEFT key -> System Navigation", + new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT}, + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_NAVIGATION, + KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT, + 0}, + {"SYSTEM_NAVIGATION_RIGHT key -> System Navigation", + new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT}, + KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_NAVIGATION, + KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT, 0}, + {"SLEEP key -> System Sleep", new int[]{KeyEvent.KEYCODE_SLEEP}, + KeyGestureEvent.KEY_GESTURE_TYPE_SLEEP, KeyEvent.KEYCODE_SLEEP, 0}, + {"SOFT_SLEEP key -> System Sleep", new int[]{KeyEvent.KEYCODE_SOFT_SLEEP}, + KeyGestureEvent.KEY_GESTURE_TYPE_SLEEP, KeyEvent.KEYCODE_SOFT_SLEEP, + 0}, + {"WAKEUP key -> System Wakeup", new int[]{KeyEvent.KEYCODE_WAKEUP}, + KeyGestureEvent.KEY_GESTURE_TYPE_WAKEUP, KeyEvent.KEYCODE_WAKEUP, 0}, + {"MEDIA_PLAY key -> Media Control", new int[]{KeyEvent.KEYCODE_MEDIA_PLAY}, + KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY, + KeyEvent.KEYCODE_MEDIA_PLAY, 0}, + {"MEDIA_PAUSE key -> Media Control", new int[]{KeyEvent.KEYCODE_MEDIA_PAUSE}, + KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY, + KeyEvent.KEYCODE_MEDIA_PAUSE, 0}, + {"MEDIA_PLAY_PAUSE key -> Media Control", + new int[]{KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE}, + KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY, + KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, 0}, + {"Meta + B -> Launch Default Browser", new int[]{META_KEY, KeyEvent.KEYCODE_B}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER, + KeyEvent.KEYCODE_B, META_ON}, + {"EXPLORER key -> Launch Default Browser", new int[]{KeyEvent.KEYCODE_EXPLORER}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER, + KeyEvent.KEYCODE_EXPLORER, 0}, + {"Meta + C -> Launch Default Contacts", new int[]{META_KEY, KeyEvent.KEYCODE_C}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS, + KeyEvent.KEYCODE_C, META_ON}, + {"CONTACTS key -> Launch Default Contacts", new int[]{KeyEvent.KEYCODE_CONTACTS}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS, + KeyEvent.KEYCODE_CONTACTS, 0}, + {"Meta + E -> Launch Default Email", new int[]{META_KEY, KeyEvent.KEYCODE_E}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL, + KeyEvent.KEYCODE_E, META_ON}, + {"ENVELOPE key -> Launch Default Email", new int[]{KeyEvent.KEYCODE_ENVELOPE}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL, + KeyEvent.KEYCODE_ENVELOPE, 0}, + {"Meta + K -> Launch Default Calendar", new int[]{META_KEY, KeyEvent.KEYCODE_K}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR, + KeyEvent.KEYCODE_K, META_ON}, + {"CALENDAR key -> Launch Default Calendar", new int[]{KeyEvent.KEYCODE_CALENDAR}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR, + KeyEvent.KEYCODE_CALENDAR, 0}, + {"Meta + P -> Launch Default Music", new int[]{META_KEY, KeyEvent.KEYCODE_P}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MUSIC, + KeyEvent.KEYCODE_P, META_ON}, + {"MUSIC key -> Launch Default Music", new int[]{KeyEvent.KEYCODE_MUSIC}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MUSIC, + KeyEvent.KEYCODE_MUSIC, 0}, + {"Meta + U -> Launch Default Calculator", new int[]{META_KEY, KeyEvent.KEYCODE_U}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR, + KeyEvent.KEYCODE_U, META_ON}, + {"CALCULATOR key -> Launch Default Calculator", + new int[]{KeyEvent.KEYCODE_CALCULATOR}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR, + KeyEvent.KEYCODE_CALCULATOR, 0}, + {"Meta + M -> Launch Default Maps", new int[]{META_KEY, KeyEvent.KEYCODE_M}, + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS, + KeyEvent.KEYCODE_M, META_ON}, + {"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}}; + } + + @Keep + private static Object[][] shortcutTestArgumentsMigratedToKeyGestureController() { // testName, testKeys, expectedKeyGestureType, expectedKey, expectedModifierState return new Object[][]{ {"Meta + H -> Open Home", new int[]{META_KEY, KeyEvent.KEYCODE_H}, @@ -64,9 +177,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { {"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}, @@ -76,9 +186,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { {"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}, @@ -138,15 +245,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { 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}, - {"VOLUME_DOWN key -> Decrease Volume", new int[]{KeyEvent.KEYCODE_VOLUME_DOWN}, - KeyGestureEvent.KEY_GESTURE_TYPE_VOLUME_DOWN, - KeyEvent.KEYCODE_VOLUME_DOWN, 0}, - {"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, @@ -170,9 +268,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { {"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, @@ -194,92 +289,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { {"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}, - {"TV_POWER key -> Toggle Power", new int[]{KeyEvent.KEYCODE_TV_POWER}, - KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_POWER, - KeyEvent.KEYCODE_TV_POWER, 0}, - {"SYSTEM_NAVIGATION_DOWN key -> System Navigation", - new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN}, - KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_NAVIGATION, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN, - 0}, - {"SYSTEM_NAVIGATION_UP key -> System Navigation", - new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP}, - KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_NAVIGATION, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_UP, - 0}, - {"SYSTEM_NAVIGATION_LEFT key -> System Navigation", - new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT}, - KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_NAVIGATION, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT, - 0}, - {"SYSTEM_NAVIGATION_RIGHT key -> System Navigation", - new int[]{KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT}, - KeyGestureEvent.KEY_GESTURE_TYPE_SYSTEM_NAVIGATION, - KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT, 0}, - {"SLEEP key -> System Sleep", new int[]{KeyEvent.KEYCODE_SLEEP}, - KeyGestureEvent.KEY_GESTURE_TYPE_SLEEP, KeyEvent.KEYCODE_SLEEP, 0}, - {"SOFT_SLEEP key -> System Sleep", new int[]{KeyEvent.KEYCODE_SOFT_SLEEP}, - KeyGestureEvent.KEY_GESTURE_TYPE_SLEEP, KeyEvent.KEYCODE_SOFT_SLEEP, - 0}, - {"WAKEUP key -> System Wakeup", new int[]{KeyEvent.KEYCODE_WAKEUP}, - KeyGestureEvent.KEY_GESTURE_TYPE_WAKEUP, KeyEvent.KEYCODE_WAKEUP, 0}, - {"MEDIA_PLAY key -> Media Control", new int[]{KeyEvent.KEYCODE_MEDIA_PLAY}, - KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY, - KeyEvent.KEYCODE_MEDIA_PLAY, 0}, - {"MEDIA_PAUSE key -> Media Control", new int[]{KeyEvent.KEYCODE_MEDIA_PAUSE}, - KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY, - KeyEvent.KEYCODE_MEDIA_PAUSE, 0}, - {"MEDIA_PLAY_PAUSE key -> Media Control", - new int[]{KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE}, - KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY, - KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, 0}, - {"Meta + B -> Launch Default Browser", new int[]{META_KEY, KeyEvent.KEYCODE_B}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER, - KeyEvent.KEYCODE_B, META_ON}, - {"EXPLORER key -> Launch Default Browser", new int[]{KeyEvent.KEYCODE_EXPLORER}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER, - KeyEvent.KEYCODE_EXPLORER, 0}, - {"Meta + C -> Launch Default Contacts", new int[]{META_KEY, KeyEvent.KEYCODE_C}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS, - KeyEvent.KEYCODE_C, META_ON}, - {"CONTACTS key -> Launch Default Contacts", new int[]{KeyEvent.KEYCODE_CONTACTS}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS, - KeyEvent.KEYCODE_CONTACTS, 0}, - {"Meta + E -> Launch Default Email", new int[]{META_KEY, KeyEvent.KEYCODE_E}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL, - KeyEvent.KEYCODE_E, META_ON}, - {"ENVELOPE key -> Launch Default Email", new int[]{KeyEvent.KEYCODE_ENVELOPE}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL, - KeyEvent.KEYCODE_ENVELOPE, 0}, - {"Meta + K -> Launch Default Calendar", new int[]{META_KEY, KeyEvent.KEYCODE_K}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR, - KeyEvent.KEYCODE_K, META_ON}, - {"CALENDAR key -> Launch Default Calendar", new int[]{KeyEvent.KEYCODE_CALENDAR}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR, - KeyEvent.KEYCODE_CALENDAR, 0}, - {"Meta + P -> Launch Default Music", new int[]{META_KEY, KeyEvent.KEYCODE_P}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MUSIC, - KeyEvent.KEYCODE_P, META_ON}, - {"MUSIC key -> Launch Default Music", new int[]{KeyEvent.KEYCODE_MUSIC}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MUSIC, - KeyEvent.KEYCODE_MUSIC, 0}, - {"Meta + U -> Launch Default Calculator", new int[]{META_KEY, KeyEvent.KEYCODE_U}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR, - KeyEvent.KEYCODE_U, META_ON}, - {"CALCULATOR key -> Launch Default Calculator", - new int[]{KeyEvent.KEYCODE_CALCULATOR}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR, - KeyEvent.KEYCODE_CALCULATOR, 0}, - {"Meta + M -> Launch Default Maps", new int[]{META_KEY, KeyEvent.KEYCODE_M}, - KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS, - KeyEvent.KEYCODE_M, META_ON}, - {"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}, {"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 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { 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 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @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 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { } @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 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase { 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 43171f847818..c186a0355588 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { */ @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 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { * 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 50b7db434267..9e47a008592c 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 @@ class ShortcutKeyTestBase { 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 @@ class ShortcutKeyTestBase { 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 98401b33840f..1aa908792c0e 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 @@ class TestPhoneWindowManager { 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 @@ class TestPhoneWindowManager { 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 @@ class TestPhoneWindowManager { } + 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 8227ed915c8e..92205f391f32 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 @@ class AppCompatActivityRobot { 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 21fac9bcd1e4..d8373c5dc3d6 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 @@ public class AppCompatUtilsTest extends WindowTestsBase { }); } + @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 @@ public class AppCompatUtilsTest extends WindowTestsBase { 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 @@ public class AppCompatUtilsTest extends WindowTestsBase { 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 @@ public class AppCompatUtilsTest extends WindowTestsBase { 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 e0344d73f540..df17cd1d24b7 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 @@ public class RecentTasksTest extends WindowTestsBase { } @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 4b03483d43b9..e4512c31069a 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 @@ package com.android.server.wm; 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 @@ public class TaskTests extends WindowTestsBase { } @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 @@ public class TaskTests extends WindowTestsBase { } @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 eebb487d16cd..9e9874b32893 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 @@ class TestWindowManagerPolicy implements WindowManagerPolicy { } @Override - public KeyEvent dispatchUnhandledKey(IBinder focusedToken, KeyEvent event, int policyFlags) { - return null; + public boolean interceptUnhandledKey(KeyEvent event, IBinder focusedToken) { + return false; } @Override diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java index 6c1e1a428fb8..129494517cd6 100644 --- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java +++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java @@ -1027,28 +1027,36 @@ public class UsbDeviceManager implements ActivityTaskManagerInternal.ScreenObser boolean enabled = (mCurrentFunctions & UsbManager.FUNCTION_MIDI) != 0; if (enabled != mMidiEnabled) { if (enabled) { + boolean midiDeviceFound = false; if (android.hardware.usb.flags.Flags.enableUsbSysfsMidiIdentification()) { try { getMidiCardDevice(); + midiDeviceFound = true; } catch (FileNotFoundException e) { - Slog.e(TAG, "could not identify MIDI device", e); - enabled = false; + Slog.w(TAG, "could not identify MIDI device", e); } - } else { + } + // For backward compatibility with older kernels without + // https://lore.kernel.org/r/20240307030922.3573161-1-royluo@google.com + if (!midiDeviceFound) { Scanner scanner = null; try { scanner = new Scanner(new File(MIDI_ALSA_PATH)); mMidiCard = scanner.nextInt(); mMidiDevice = scanner.nextInt(); + midiDeviceFound = true; } catch (FileNotFoundException e) { Slog.e(TAG, "could not open MIDI file", e); - enabled = false; } finally { if (scanner != null) { scanner.close(); } } } + if (!midiDeviceFound) { + Slog.e(TAG, "Failed to enable MIDI function"); + enabled = false; + } } mMidiEnabled = enabled; } diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java index dc5f6e960a2b..fb031bd01673 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 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { 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 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener { 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 862aff9be9ce..2bb86bc305a7 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 @@ public class SoundTriggerService extends SystemService { 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 ba360070abc3..4ae06a4f9812 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 @@ package com.android.server.input 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.IKeyGestureHandler 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 @@ class KeyGestureControllerTests { 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 @@ class KeyGestureControllerTests { 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 @@ class KeyGestureControllerTests { // 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 @@ class KeyGestureControllerTests { @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 @@ class KeyGestureControllerTests { 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 @@ class KeyGestureControllerTests { 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 @@ class KeyGestureControllerTests { 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 @@ class KeyGestureControllerTests { 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 } + 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() - for (key in testKeys) { - sendKeys(intArrayOf(key), /* assertAllConsumed = */ true) - } + 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 @@ class KeyGestureControllerTests { 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 b5258dfc9c3c..60fa52f85e34 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 @@ public class TouchpadDebugViewTest { // 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 7f58fceee24d..f6e5221b721b 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 df1d51e37660..064b4617b0a2 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -346,6 +346,21 @@ void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& 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 a274f047586c..0d261abd728d 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -71,6 +71,17 @@ enum class ResourceType { 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 @@ struct ResourceFile { // 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 a5aecc855707..fce6aa7c80d9 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -107,9 +107,10 @@ struct ParsedResource { 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 @@ static bool AddResourcesToTable(ResourceTable* table, android::IDiagnostics* dia } 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 @@ static bool AddResourcesToTable(ResourceTable* table, android::IDiagnostics* dia 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 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, {"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 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, // 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 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, // 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 @@ bool ResourceParser::ParseResource(xml::XmlPullParser* parser, 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 @@ bool ResourceParser::ParseArrayImpl(xml::XmlPullParser* parser, 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 442dea89ef40..90690d522ef2 100644 --- a/tools/aapt2/ResourceParser.h +++ b/tools/aapt2/ResourceParser.h @@ -57,6 +57,11 @@ struct ResourceParserOptions { 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 @@ class ResourceParser { 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 97514599c0b1..5435cba290fc 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -101,6 +101,21 @@ struct lt_config_key_ref { } }; +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 @@ std::vector<ResourceConfigValue*> ResourceEntry::FindAllValues(const ConfigDescr 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 @@ struct EntryViewComparer { } }; -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 @@ void InsertEntryIntoTableView(ResourceTableView& table, const ResourceTablePacka 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 @@ const ResourceConfigValue* ResourceTableEntryView::FindValue(const ConfigDescrip 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 @@ ResourceTableView ResourceTable::GetPartitionedView(const ResourceTableViewOptio 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 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) 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 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag) } 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 @@ NewResourceBuilder& NewResourceBuilder::SetAllowMangled(bool allow_mangled) { 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 cba6b70cfbd6..b0e185536d16 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -136,6 +136,9 @@ class ResourceEntry { // 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 @@ class ResourceEntry { 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 @@ struct ResourceTableEntryView { 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 @@ struct NewResource { 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 @@ struct NewResourceBuilder { 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 b75e87c90128..723cfc0e035b 100644 --- a/tools/aapt2/ResourceValues.cpp +++ b/tools/aapt2/ResourceValues.cpp @@ -1102,6 +1102,7 @@ template <typename T> 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 a1b1839b19ef..e000c653b87a 100644 --- a/tools/aapt2/ResourceValues.h +++ b/tools/aapt2/ResourceValues.h @@ -65,10 +65,21 @@ class Value { 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 @@ class Value { 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 5c6408940b34..a0f60b62db3a 100644 --- a/tools/aapt2/Resources.proto +++ b/tools/aapt2/Resources.proto @@ -240,6 +240,9 @@ message Entry { // 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 @@ message Item { // 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 b0ed3da33368..f4735a2f6ce7 100644 --- a/tools/aapt2/ResourcesInternal.proto +++ b/tools/aapt2/ResourcesInternal.proto @@ -49,4 +49,9 @@ message CompiledFile { // 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 2a978a5153ca..52372fa38525 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -67,6 +67,7 @@ struct ResourcePathData { 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 @@ static std::optional<ResourcePathData> ExtractResourcePathData(const std::string 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 @@ static std::optional<ResourcePathData> ExtractResourcePathData(const std::string std::string(dir_str), std::string(name), std::string(extension), + std::move(flag_name), std::string(config_str), config}; } @@ -142,6 +160,9 @@ static std::string BuildIntermediateContainerFilename(const ResourcePathData& da 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 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, << "failed to open file: " << fin->GetError()); return false; } - // Parse the values file from XML. xml::XmlPullParser xml_parser(fin.get()); @@ -176,6 +196,18 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, // 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 @@ static bool CompileXml(IAaptContext* context, const CompileOptions& options, 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 @@ static bool CompilePng(IAaptContext* context, const CompileOptions& options, 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 6da3176b2bee..d3750a6100d3 100644 --- a/tools/aapt2/cmd/Diff.cpp +++ b/tools/aapt2/cmd/Diff.cpp @@ -138,6 +138,22 @@ static bool EmitResourceEntryDiff(IAaptContext* context, LoadedApk* apk_a, } } + 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 @@ static bool EmitResourceEntryDiff(IAaptContext* context, LoadedApk* apk_a, 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 7739171b347f..08f8f0d85807 100644 --- a/tools/aapt2/cmd/Util.cpp +++ b/tools/aapt2/cmd/Util.cpp @@ -34,6 +34,44 @@ using ::android::base::StringPrintf; 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 6b8813b34082..d32e532b86a8 100644 --- a/tools/aapt2/cmd/Util.h +++ b/tools/aapt2/cmd/Util.h @@ -49,6 +49,12 @@ struct FeatureFlagProperties { 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 55f5e5668a16..8583cadff6d2 100644 --- a/tools/aapt2/format/proto/ProtoDeserialize.cpp +++ b/tools/aapt2/format/proto/ProtoDeserialize.cpp @@ -536,6 +536,34 @@ static bool DeserializePackageFromPb(const pb::Package& pb_package, const ResStr 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 @@ bool DeserializeCompiledFileFromPb(const pb::internal::CompiledFile& pb_file, 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 @@ std::unique_ptr<Value> DeserializeValueFromPb(const pb::Value& pb_value, 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 @@ std::unique_ptr<Item> DeserializeItemFromPb(const pb::Item& pb_item, 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 5772b3b0b3e6..d83fe916ee95 100644 --- a/tools/aapt2/format/proto/ProtoSerialize.cpp +++ b/tools/aapt2/format/proto/ProtoSerialize.cpp @@ -427,6 +427,14 @@ void SerializeTableToPb(const ResourceTable& table, pb::ResourceTable* out_table 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 @@ void SerializeValueToPb(const Value& value, pb::Value* out_value, android::Strin } 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 @@ void SerializeItemToPb(const Item& item, pb::Item* out_item) { 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 @@ void SerializeCompiledFileToPb(const ResourceFile& file, pb::internal::CompiledF 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 c456e5c296d2..7160b35033da 100644 --- a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp @@ -31,13 +31,29 @@ genrule { "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(test.package.falseFlag)/drawable/removedpng.png b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/drawable/removedpng.png Binary files differnew file mode 100644 index 000000000000..8a9e6984be96 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/drawable/removedpng.png diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/layout/layout2.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/layout/layout2.xml new file mode 100644 index 000000000000..dec5de72925a --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/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(test.package.falseFlag)/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/values/bools.xml new file mode 100644 index 000000000000..c46c4d4d8546 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.falseFlag)/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(test.package.trueFlag)/layout/layout3.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.trueFlag)/layout/layout3.xml new file mode 100644 index 000000000000..5aeee0ee1e28 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/flag(test.package.trueFlag)/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 000000000000..dec5de72925a --- /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 1ed0c8a5f1e6..35975ed1274a 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(!test.package.falseFlag)/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.falseFlag)/bools.xml new file mode 100644 index 000000000000..a63749c6ed7e --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.falseFlag)/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(!test.package.trueFlag)/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.trueFlag)/bools.xml new file mode 100644 index 000000000000..bb5526e69f97 --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(!test.package.trueFlag)/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(test.package.trueFlag)/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(test.package.trueFlag)/bools.xml new file mode 100644 index 000000000000..eba780e88c9a --- /dev/null +++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/flag(test.package.trueFlag)/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 3db37c2fa6f8..629300838bbe 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 @@ TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTable) { 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 @@ TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTableChunks) { 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 @@ TEST_F(FlaggedResourcesTest, DisabledResourcesInRJava) { 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 37a039e9528f..1bef5f8b17f6 100644 --- a/tools/aapt2/link/TableMerger.cpp +++ b/tools/aapt2/link/TableMerger.cpp @@ -321,6 +321,30 @@ bool TableMerger::DoMerge(const android::Source& src, ResourceTablePackage* src_ } } } + + // 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 @@ bool TableMerger::MergeFile(const ResourceFile& file_desc, bool overlay, io::IFi 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 cdf245341844..c7dd4c90e67f 100644 --- a/tools/aapt2/test/Common.cpp +++ b/tools/aapt2/test/Common.cpp @@ -21,23 +21,6 @@ using android::ConfigDescription; 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 04379804d8ee..b06c4329488e 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 b91abe572306..570bcf16c92c 100644 --- a/tools/aapt2/test/Fixture.cpp +++ b/tools/aapt2/test/Fixture.cpp @@ -91,10 +91,13 @@ void TestDirectoryFixture::WriteFile(const std::string& path, const std::string& } 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 14298d1678f0..178d01156f32 100644 --- a/tools/aapt2/test/Fixture.h +++ b/tools/aapt2/test/Fixture.h @@ -73,7 +73,8 @@ class CommandTestFixture : public TestDirectoryFixture { // 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); |