diff options
189 files changed, 7807 insertions, 1996 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 497619ae0613..ad849002cca1 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -21,6 +21,7 @@ aconfig_declarations_group { java_aconfig_libraries: [ // !!! KEEP THIS LIST ALPHABETICAL !!! "aconfig_mediacodec_flags_java_lib", + "aconfig_settingslib_flags_java_lib", "aconfig_trade_in_mode_flags_java_lib", "android-sdk-flags-java", "android.adaptiveauth.flags-aconfig-java", @@ -1757,3 +1758,19 @@ cc_aconfig_library { ], min_sdk_version: "apex_inherit", } + +// Settings Lib +aconfig_declarations { + name: "aconfig_settingslib_flags", + package: "com.android.settingslib.flags", + container: "system", + srcs: [ + "packages/SettingsLib/aconfig/settingslib.aconfig", + ], +} + +java_aconfig_library { + name: "aconfig_settingslib_flags_java_lib", + aconfig_declarations: "aconfig_settingslib_flags", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/Android.bp b/Android.bp index 26d0d65f329c..9cb3067096cc 100644 --- a/Android.bp +++ b/Android.bp @@ -220,7 +220,7 @@ java_library { "android.hardware.contexthub-V1.0-java", "android.hardware.contexthub-V1.1-java", "android.hardware.contexthub-V1.2-java", - "android.hardware.contexthub-V3-java", + "android.hardware.contexthub-V4-java", "android.hardware.gnss-V1.0-java", "android.hardware.gnss-V2.1-java", "android.hardware.health-V1.0-java-constants", diff --git a/core/api/current.txt b/core/api/current.txt index 42d4c145343c..664b3dd125ef 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -263,6 +263,7 @@ package android { field public static final String READ_SMS = "android.permission.READ_SMS"; field public static final String READ_SYNC_SETTINGS = "android.permission.READ_SYNC_SETTINGS"; field public static final String READ_SYNC_STATS = "android.permission.READ_SYNC_STATS"; + field @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final String READ_SYSTEM_PREFERENCES = "android.permission.READ_SYSTEM_PREFERENCES"; field public static final String READ_VOICEMAIL = "com.android.voicemail.permission.READ_VOICEMAIL"; field public static final String REBOOT = "android.permission.REBOOT"; field public static final String RECEIVE_BOOT_COMPLETED = "android.permission.RECEIVE_BOOT_COMPLETED"; @@ -313,6 +314,7 @@ package android { field public static final String SYSTEM_ALERT_WINDOW = "android.permission.SYSTEM_ALERT_WINDOW"; field public static final String TRANSMIT_IR = "android.permission.TRANSMIT_IR"; field public static final String TURN_SCREEN_ON = "android.permission.TURN_SCREEN_ON"; + field @FlaggedApi("android.app.enable_tv_implicit_enter_pip_restriction") public static final String TV_IMPLICIT_ENTER_PIP = "android.permission.TV_IMPLICIT_ENTER_PIP"; field public static final String UNINSTALL_SHORTCUT = "com.android.launcher.permission.UNINSTALL_SHORTCUT"; field public static final String UPDATE_DEVICE_STATS = "android.permission.UPDATE_DEVICE_STATS"; field public static final String UPDATE_PACKAGES_WITHOUT_USER_ACTION = "android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION"; @@ -334,6 +336,7 @@ package android { field public static final String WRITE_SECURE_SETTINGS = "android.permission.WRITE_SECURE_SETTINGS"; field public static final String WRITE_SETTINGS = "android.permission.WRITE_SETTINGS"; field public static final String WRITE_SYNC_SETTINGS = "android.permission.WRITE_SYNC_SETTINGS"; + field @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final String WRITE_SYSTEM_PREFERENCES = "android.permission.WRITE_SYSTEM_PREFERENCES"; field public static final String WRITE_VOICEMAIL = "com.android.voicemail.permission.WRITE_VOICEMAIL"; } @@ -19777,6 +19780,9 @@ package android.hardware.camera2 { field public static final int EDGE_MODE_HIGH_QUALITY = 2; // 0x2 field public static final int EDGE_MODE_OFF = 0; // 0x0 field public static final int EDGE_MODE_ZERO_SHUTTER_LAG = 3; // 0x3 + field @FlaggedApi("com.android.internal.camera.flags.night_mode_indicator") public static final int EXTENSION_NIGHT_MODE_INDICATOR_OFF = 1; // 0x1 + field @FlaggedApi("com.android.internal.camera.flags.night_mode_indicator") public static final int EXTENSION_NIGHT_MODE_INDICATOR_ON = 2; // 0x2 + field @FlaggedApi("com.android.internal.camera.flags.night_mode_indicator") public static final int EXTENSION_NIGHT_MODE_INDICATOR_UNKNOWN = 0; // 0x0 field public static final int FLASH_MODE_OFF = 0; // 0x0 field public static final int FLASH_MODE_SINGLE = 1; // 0x1 field public static final int FLASH_MODE_TORCH = 2; // 0x2 @@ -20076,6 +20082,7 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> DISTORTION_CORRECTION_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EDGE_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EXTENSION_CURRENT_TYPE; + field @FlaggedApi("com.android.internal.camera.flags.night_mode_indicator") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EXTENSION_NIGHT_MODE_INDICATOR; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> EXTENSION_STRENGTH; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> FLASH_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> FLASH_STATE; @@ -23973,6 +23980,7 @@ package android.media { field public static final String KEY_MPEGH_COMPATIBLE_SETS = "mpegh-compatible-sets"; field public static final String KEY_MPEGH_PROFILE_LEVEL_INDICATION = "mpegh-profile-level-indication"; field public static final String KEY_MPEGH_REFERENCE_CHANNEL_LAYOUT = "mpegh-reference-channel-layout"; + field @FlaggedApi("android.media.codec.num_input_slots") public static final String KEY_NUM_SLOTS = "num-slots"; field public static final String KEY_OPERATING_RATE = "operating-rate"; field public static final String KEY_OUTPUT_REORDER_DEPTH = "output-reorder-depth"; field public static final String KEY_PCM_ENCODING = "pcm-encoding"; @@ -40371,7 +40379,7 @@ package android.security.keystore { method @NonNull public android.security.keystore.KeyProtection.Builder setUserPresenceRequired(boolean); } - @FlaggedApi("android.security.keystore_grant_api") public class KeyStoreManager { + @FlaggedApi("android.security.keystore_grant_api") public final class KeyStoreManager { method @NonNull public java.util.List<java.security.cert.X509Certificate> getGrantedCertificateChainFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException; method @NonNull public java.security.Key getGrantedKeyFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException; method @NonNull public java.security.KeyPair getGrantedKeyPairFromId(long) throws android.security.keystore.KeyPermanentlyInvalidatedException, java.security.UnrecoverableKeyException; @@ -41999,6 +42007,174 @@ package android.service.restrictions { } +package android.service.settings.preferences { + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class GetValueRequest implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public String getPreferenceKey(); + method @NonNull public String getScreenKey(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.GetValueRequest> CREATOR; + } + + public static final class GetValueRequest.Builder { + ctor public GetValueRequest.Builder(@NonNull String, @NonNull String); + method @NonNull public android.service.settings.preferences.GetValueRequest build(); + } + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class GetValueResult implements android.os.Parcelable { + method public int describeContents(); + method @Nullable public android.service.settings.preferences.SettingsPreferenceMetadata getMetadata(); + method public int getResultCode(); + method @Nullable public android.service.settings.preferences.SettingsPreferenceValue getValue(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.GetValueResult> CREATOR; + field public static final int RESULT_DISALLOW = 4; // 0x4 + field public static final int RESULT_INTERNAL_ERROR = 6; // 0x6 + field public static final int RESULT_INVALID_REQUEST = 5; // 0x5 + field public static final int RESULT_OK = 0; // 0x0 + field public static final int RESULT_REQUIRE_APP_PERMISSION = 3; // 0x3 + field public static final int RESULT_UNAVAILABLE = 2; // 0x2 + field public static final int RESULT_UNSUPPORTED = 1; // 0x1 + } + + public static final class GetValueResult.Builder { + ctor public GetValueResult.Builder(int); + method @NonNull public android.service.settings.preferences.GetValueResult build(); + method @NonNull public android.service.settings.preferences.GetValueResult.Builder setMetadata(@Nullable android.service.settings.preferences.SettingsPreferenceMetadata); + method @NonNull public android.service.settings.preferences.GetValueResult.Builder setValue(@Nullable android.service.settings.preferences.SettingsPreferenceValue); + } + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class MetadataRequest implements android.os.Parcelable { + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.MetadataRequest> CREATOR; + } + + public static final class MetadataRequest.Builder { + ctor public MetadataRequest.Builder(); + method @NonNull public android.service.settings.preferences.MetadataRequest build(); + } + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class MetadataResult implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.List<android.service.settings.preferences.SettingsPreferenceMetadata> getMetadataList(); + method public int getResultCode(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.MetadataResult> CREATOR; + field public static final int RESULT_INTERNAL_ERROR = 2; // 0x2 + field public static final int RESULT_OK = 0; // 0x0 + field public static final int RESULT_UNSUPPORTED = 1; // 0x1 + } + + public static final class MetadataResult.Builder { + ctor public MetadataResult.Builder(int); + method @NonNull public android.service.settings.preferences.MetadataResult build(); + method @NonNull public android.service.settings.preferences.MetadataResult.Builder setMetadataList(@NonNull java.util.List<android.service.settings.preferences.SettingsPreferenceMetadata>); + } + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SetValueRequest implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public String getPreferenceKey(); + method @NonNull public android.service.settings.preferences.SettingsPreferenceValue getPreferenceValue(); + method @NonNull public String getScreenKey(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.SetValueRequest> CREATOR; + } + + public static final class SetValueRequest.Builder { + ctor public SetValueRequest.Builder(@NonNull String, @NonNull String, @NonNull android.service.settings.preferences.SettingsPreferenceValue); + method @NonNull public android.service.settings.preferences.SetValueRequest build(); + } + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SetValueResult implements android.os.Parcelable { + method public int describeContents(); + method public int getResultCode(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.SetValueResult> CREATOR; + field public static final int RESULT_DISABLED = 2; // 0x2 + field public static final int RESULT_DISALLOW = 7; // 0x7 + field public static final int RESULT_INTERNAL_ERROR = 9; // 0x9 + field public static final int RESULT_INVALID_REQUEST = 8; // 0x8 + field public static final int RESULT_OK = 0; // 0x0 + field public static final int RESULT_REQUIRE_APP_PERMISSION = 5; // 0x5 + field public static final int RESULT_REQUIRE_USER_CONSENT = 6; // 0x6 + field public static final int RESULT_RESTRICTED = 3; // 0x3 + field public static final int RESULT_UNAVAILABLE = 4; // 0x4 + field public static final int RESULT_UNSUPPORTED = 1; // 0x1 + } + + public static final class SetValueResult.Builder { + ctor public SetValueResult.Builder(int); + method @NonNull public android.service.settings.preferences.SetValueResult build(); + } + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SettingsPreferenceMetadata implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.List<java.lang.String> getBreadcrumbs(); + method @NonNull public android.os.Bundle getExtras(); + method @NonNull public String getKey(); + method @Nullable public android.app.PendingIntent getLaunchIntent(); + method @NonNull public java.util.List<java.lang.String> getReadPermissions(); + method @NonNull public String getScreenKey(); + method @Nullable public String getSummary(); + method @Nullable public String getTitle(); + method @NonNull public java.util.List<java.lang.String> getWritePermissions(); + method public int getWriteSensitivity(); + method public boolean isAvailable(); + method public boolean isEnabled(); + method public boolean isRestricted(); + method public boolean isWritable(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.SettingsPreferenceMetadata> CREATOR; + field public static final int INTENT_ONLY = 2; // 0x2 + field public static final int NOT_SENSITIVE = 0; // 0x0 + field public static final int SENSITIVE = 1; // 0x1 + } + + public static final class SettingsPreferenceMetadata.Builder { + ctor public SettingsPreferenceMetadata.Builder(@NonNull String, @NonNull String); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata build(); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setAvailable(boolean); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setBreadcrumbs(@NonNull java.util.List<java.lang.String>); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setEnabled(boolean); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setExtras(@NonNull android.os.Bundle); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setLaunchIntent(@Nullable android.app.PendingIntent); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setReadPermissions(@NonNull java.util.List<java.lang.String>); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setRestricted(boolean); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setSummary(@Nullable String); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setTitle(@Nullable String); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setWritable(boolean); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setWritePermissions(@NonNull java.util.List<java.lang.String>); + method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setWriteSensitivity(int); + } + + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SettingsPreferenceValue implements android.os.Parcelable { + method public int describeContents(); + method public boolean getBooleanValue(); + method public double getDoubleValue(); + method public long getLongValue(); + method @Nullable public String getStringValue(); + method public int getType(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.SettingsPreferenceValue> CREATOR; + field public static final int TYPE_BOOLEAN = 0; // 0x0 + field public static final int TYPE_DOUBLE = 2; // 0x2 + field public static final int TYPE_LONG = 1; // 0x1 + field public static final int TYPE_STRING = 3; // 0x3 + } + + public static final class SettingsPreferenceValue.Builder { + ctor public SettingsPreferenceValue.Builder(int); + method @NonNull public android.service.settings.preferences.SettingsPreferenceValue build(); + method @NonNull public android.service.settings.preferences.SettingsPreferenceValue.Builder setBooleanValue(boolean); + method @NonNull public android.service.settings.preferences.SettingsPreferenceValue.Builder setDoubleValue(double); + method @NonNull public android.service.settings.preferences.SettingsPreferenceValue.Builder setLongValue(long); + method @NonNull public android.service.settings.preferences.SettingsPreferenceValue.Builder setStringValue(@Nullable String); + } + +} + package android.service.textservice { public abstract class SpellCheckerService extends android.app.Service { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 2a01ca082832..a9b181d51fb4 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -389,6 +389,7 @@ package android { field public static final String START_CROSS_PROFILE_ACTIVITIES = "android.permission.START_CROSS_PROFILE_ACTIVITIES"; field public static final String START_REVIEW_PERMISSION_DECISIONS = "android.permission.START_REVIEW_PERMISSION_DECISIONS"; field public static final String START_TASKS_FROM_RECENTS = "android.permission.START_TASKS_FROM_RECENTS"; + field @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public static final String START_VIBRATION_SESSIONS = "android.permission.START_VIBRATION_SESSIONS"; field public static final String STATUS_BAR_SERVICE = "android.permission.STATUS_BAR_SERVICE"; field public static final String STOP_APP_SWITCHES = "android.permission.STOP_APP_SWITCHES"; field public static final String SUBSTITUTE_NOTIFICATION_APP_NAME = "android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"; @@ -11660,8 +11661,11 @@ package android.os { public abstract class Vibrator { method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener); method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.Vibrator.OnVibratorStateChangedListener); + method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public boolean areVendorEffectsSupported(); + method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public boolean areVendorSessionsSupported(); method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public boolean isVibrating(); method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void removeVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener); + method @FlaggedApi("android.os.vibrator.vendor_vibration_effects") @RequiresPermission(allOf={android.Manifest.permission.VIBRATE, android.Manifest.permission.VIBRATE_VENDOR_EFFECTS, android.Manifest.permission.START_VIBRATION_SESSIONS}) public void startVendorSession(@NonNull android.os.VibrationAttributes, @Nullable String, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.vibrator.VendorVibrationSession.Callback); } public static interface Vibrator.OnVibratorStateChangedListener { @@ -11813,6 +11817,28 @@ package android.os.storage { } +package android.os.vibrator { + + @FlaggedApi("android.os.vibrator.vendor_vibration_effects") public final class VendorVibrationSession implements java.lang.AutoCloseable { + method public void cancel(); + method public void close(); + method @RequiresPermission(android.Manifest.permission.VIBRATE) public void vibrate(@NonNull android.os.VibrationEffect, @Nullable String); + field public static final int STATUS_CANCELED = 4; // 0x4 + field public static final int STATUS_IGNORED = 2; // 0x2 + field public static final int STATUS_SUCCESS = 1; // 0x1 + field public static final int STATUS_UNKNOWN = 0; // 0x0 + field public static final int STATUS_UNKNOWN_ERROR = 5; // 0x5 + field public static final int STATUS_UNSUPPORTED = 3; // 0x3 + } + + public static interface VendorVibrationSession.Callback { + method public void onFinished(int); + method public void onFinishing(); + method public void onStarted(@NonNull android.os.vibrator.VendorVibrationSession); + } + +} + package android.os.vibrator.persistence { @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class ParsedVibration { diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 7a1c759a3ec4..3fccc17e1bf1 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -30,6 +30,7 @@ import static com.android.sdksandbox.flags.Flags.sandboxActivitySdkBasedContext; import static java.lang.Character.MIN_VALUE; +import android.Manifest; import android.annotation.AnimRes; import android.annotation.CallSuper; import android.annotation.CallbackExecutor; @@ -3193,6 +3194,16 @@ public class Activity extends ContextThemeWrapper return ActivityTaskManager.getMaxNumPictureInPictureActions(this); } + private boolean isImplicitEnterPipProhibited() { + PackageManager pm = getPackageManager(); + if (android.app.Flags.enableTvImplicitEnterPipRestriction()) { + return pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + && pm.checkPermission(Manifest.permission.TV_IMPLICIT_ENTER_PIP, + getPackageName()) == PackageManager.PERMISSION_DENIED; + } + return false; + } + /** * @return Whether this device supports picture-in-picture. */ @@ -9192,6 +9203,8 @@ public class Activity extends ContextThemeWrapper } dispatchActivityPreResumed(); + mCanEnterPictureInPicture = true; + mFragments.execPendingActions(); mLastNonConfigurationInstances = null; @@ -9243,6 +9256,11 @@ public class Activity extends ContextThemeWrapper Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "performPause:" + mComponent.getClassName()); } + + if (isImplicitEnterPipProhibited()) { + mCanEnterPictureInPicture = false; + } + dispatchActivityPrePaused(); mDoReportFullyDrawn = false; mFragments.dispatchPause(); @@ -9265,6 +9283,10 @@ public class Activity extends ContextThemeWrapper final void performUserLeaving() { onUserInteraction(); + + if (isImplicitEnterPipProhibited()) { + mCanEnterPictureInPicture = false; + } onUserLeaveHint(); } diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java index 68794588afc5..009cd7249dcd 100644 --- a/core/java/android/app/AppCompatTaskInfo.java +++ b/core/java/android/app/AppCompatTaskInfo.java @@ -101,7 +101,6 @@ public class AppCompatTaskInfo implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { FLAG_UNDEFINED, - FLAG_BASE, FLAG_LETTERBOX_EDU_ENABLED, FLAG_ELIGIBLE_FOR_LETTERBOX_EDU, FLAG_LETTERBOXED, @@ -115,6 +114,10 @@ public class AppCompatTaskInfo implements Parcelable { }) public @interface TopActivityFlag {} + /** + * A combination of {@link TopActivityFlag}s that have been enabled through + * {@link #setTopActivityFlag}. + */ @TopActivityFlag private int mTopActivityFlags; diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 2cf718ef364f..3ae60d71facd 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -1167,6 +1167,7 @@ class ContextImpl extends Context { @Override public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) { try { + intent.collectExtraIntentKeys(); ActivityTaskManager.getService().startActivityAsUser( mMainThread.getApplicationThread(), getOpPackageName(), getAttributionTag(), intent, intent.resolveTypeIfNeeded(getContentResolver()), diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index c6c0395d1b93..0381ee0e25ac 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -155,7 +155,7 @@ public class Notification implements Parcelable FOREGROUND_SERVICE_IMMEDIATE, FOREGROUND_SERVICE_DEFERRED }) - public @interface ServiceNotificationPolicy {}; + public @interface ServiceNotificationPolicy {} /** * If the Notification associated with starting a foreground service has been @@ -1754,10 +1754,6 @@ public class Notification implements Parcelable private Icon mSmallIcon; @UnsupportedAppUsage private Icon mLargeIcon; - private Icon mAppIcon; - - /** Cache for whether the notification was posted by a headless system app. */ - private Boolean mBelongsToHeadlessSystemApp = null; @UnsupportedAppUsage private String mChannelId; @@ -3247,86 +3243,6 @@ public class Notification implements Parcelable } /** - * Whether this notification was posted by a headless system app. - * - * If we don't have enough information to figure this out, this will return false. Therefore, - * false negatives are possible, but false positives should not be. - * - * @hide - */ - public boolean belongsToHeadlessSystemApp(Context context) { - Trace.beginSection("Notification#belongsToHeadlessSystemApp"); - - try { - if (mBelongsToHeadlessSystemApp != null) { - return mBelongsToHeadlessSystemApp; - } - - if (context == null) { - // Without a valid context, we don't know exactly. Let's assume it doesn't belong to - // a system app, but not cache the value. - return false; - } - - ApplicationInfo info = getApplicationInfo(context); - if (info != null) { - if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { - // It's not a system app at all. - mBelongsToHeadlessSystemApp = false; - } else { - // If there's no launch intent, it's probably a headless app. - final PackageManager pm = context.getPackageManager(); - mBelongsToHeadlessSystemApp = pm.getLaunchIntentForPackage(info.packageName) - == null; - } - } else { - // If for some reason we don't have the app info, we don't know; best assume it's - // not a system app. - return false; - } - return mBelongsToHeadlessSystemApp; - } finally { - Trace.endSection(); - } - } - - /** - * Get the resource ID of the app icon from application info. - * @hide - */ - public int getHeaderAppIconRes(Context context) { - ApplicationInfo info = getApplicationInfo(context); - if (info != null) { - return info.icon; - } - return 0; - } - - /** - * Load the app icon drawable from the package manager. This could result in a binder call. - * @hide - */ - public Drawable loadHeaderAppIcon(Context context) { - Trace.beginSection("Notification#loadHeaderAppIcon"); - - try { - if (context == null) { - Log.e(TAG, "Cannot load the app icon drawable with a null context"); - return null; - } - final PackageManager pm = context.getPackageManager(); - ApplicationInfo info = getApplicationInfo(context); - if (info == null) { - Log.e(TAG, "Cannot load the app icon drawable: no application info"); - return null; - } - return pm.getApplicationIcon(info); - } finally { - Trace.endSection(); - } - } - - /** * Fetch the application info from the notification, or the context if that isn't available. */ private ApplicationInfo getApplicationInfo(Context context) { @@ -4361,55 +4277,6 @@ public class Notification implements Parcelable } /** - * The colored app icon that can replace the small icon in the notification starting in V. - * - * Before using this value, you should first check whether it's actually being used by the - * notification by calling {@link Notification#shouldUseAppIcon()}. - * - * @hide - */ - public Icon getAppIcon() { - if (mAppIcon != null) { - return mAppIcon; - } - // If the app icon hasn't been loaded yet, check if we can load it without a context. - if (extras.containsKey(EXTRA_BUILDER_APPLICATION_INFO)) { - final ApplicationInfo info = extras.getParcelable( - EXTRA_BUILDER_APPLICATION_INFO, - ApplicationInfo.class); - if (info != null) { - int appIconRes = info.icon; - if (appIconRes == 0) { - Log.w(TAG, "Failed to get the app icon: no icon in application info"); - return null; - } - mAppIcon = Icon.createWithResource(info.packageName, appIconRes); - return mAppIcon; - } else { - Log.e(TAG, "Failed to get the app icon: " - + "there's an EXTRA_BUILDER_APPLICATION_INFO in extras but it's null"); - } - } else { - Log.w(TAG, "Failed to get the app icon: no application info in extras"); - } - return null; - } - - /** - * Whether the notification is using the app icon instead of the small icon. - * @hide - */ - public boolean shouldUseAppIcon() { - if (Flags.notificationsUseAppIconInRow()) { - if (belongsToHeadlessSystemApp(/* context = */ null)) { - return false; - } - return getAppIcon() != null; - } - return false; - } - - /** * The large icon shown in this notification's content view. * @see Builder#getLargeIcon() * @see Builder#setLargeIcon(Icon) @@ -4566,19 +4433,6 @@ public class Notification implements Parcelable private static final boolean USE_ONLY_TITLE_IN_LOW_PRIORITY_SUMMARY = SystemProperties.getBoolean("notifications.only_title", true); - /** - * The lightness difference that has to be added to the primary text color to obtain the - * secondary text color when the background is light. - */ - private static final int LIGHTNESS_TEXT_DIFFERENCE_LIGHT = 20; - - /** - * The lightness difference that has to be added to the primary text color to obtain the - * secondary text color when the background is dark. - * A bit less then the above value, since it looks better on dark backgrounds. - */ - private static final int LIGHTNESS_TEXT_DIFFERENCE_DARK = -10; - private Context mContext; private Notification mN; private Bundle mUserExtras = new Bundle(); @@ -6451,36 +6305,12 @@ public class Notification implements Parcelable } private void bindSmallIcon(RemoteViews contentView, StandardTemplateParams p) { - if (Flags.notificationsUseAppIcon()) { - // Override small icon with app icon - mN.mSmallIcon = Icon.createWithResource(mContext, - mN.getHeaderAppIconRes(mContext)); - } else if (mN.mSmallIcon == null && mN.icon != 0) { + if (mN.mSmallIcon == null && mN.icon != 0) { mN.mSmallIcon = Icon.createWithResource(mContext, mN.icon); } - - boolean usingAppIcon = false; - if (Flags.notificationsUseAppIconInRow() && !mN.belongsToHeadlessSystemApp(mContext)) { - // Use the app icon in the view - int appIconRes = mN.getHeaderAppIconRes(mContext); - if (appIconRes != 0) { - mN.mAppIcon = Icon.createWithResource(mContext, appIconRes); - contentView.setImageViewIcon(R.id.icon, mN.mAppIcon); - contentView.setBoolean(R.id.icon, "setShouldShowAppIcon", true); - usingAppIcon = true; - } else { - Log.w(TAG, "bindSmallIcon: could not get the app icon"); - } - } - if (!usingAppIcon) { - contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon); - } + contentView.setImageViewIcon(R.id.icon, mN.mSmallIcon); contentView.setInt(R.id.icon, "setImageLevel", mN.iconLevel); - - // Don't change color if we're using the app icon. - if (!Flags.notificationsUseAppIcon() && !usingAppIcon) { - processSmallIconColor(mN.mSmallIcon, contentView, p); - } + processSmallIconColor(mN.mSmallIcon, contentView, p); } /** diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 04a9d13420ee..fee071b14016 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -359,3 +359,11 @@ flag { description: "Enables coexistence support for Setting MTE policy." bug: "376213673" } + +flag { + name: "enable_supervision_service_sync" + is_exported: true + namespace: "enterprise" + description: "Allows DPMS to enable or disable SupervisionService based on whether the device is being managed by the supervision role holder." + bug: "376213673" +} diff --git a/core/java/android/app/multitasking.aconfig b/core/java/android/app/multitasking.aconfig index 9a645192a155..c8455c1f439f 100644 --- a/core/java/android/app/multitasking.aconfig +++ b/core/java/android/app/multitasking.aconfig @@ -8,3 +8,11 @@ flag { description: "Enables PiP UI state callback on entering" bug: "303718131" } + +flag { + name: "enable_tv_implicit_enter_pip_restriction" + is_exported: true + namespace: "tv_system_ui" + description: "Enables restrictions to PiP entry on TV for setAutoEnterEnabled and lifecycle methods" + bug: "283115999" +} diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 0fc4291d15ab..ee93870be055 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -8,8 +8,7 @@ container: "system" flag { name: "notifications_redesign_app_icons" namespace: "systemui" - description: "Notifications Redesign: Use app icons in notification rows (not to be confused with" - " notifications_use_app_icons, notifications_use_app_icon_in_row which are just experiments)." + description: "Notifications Redesign: Use app icons in notification rows" bug: "371174789" } @@ -110,31 +109,6 @@ flag { } } -# vvv Prototypes for using app icons in notifications vvv - -flag { - name: "notifications_use_app_icon" - namespace: "systemui" - description: "Experiment to replace the small icon in a notification with the app icon. This includes the status bar, AOD, shelf and notification row itself." - bug: "335211019" -} - -flag { - name: "notifications_use_app_icon_in_row" - namespace: "systemui" - description: "Experiment to replace the small icon in a notification row with the app icon." - bug: "335211019" -} - -flag { - name: "notifications_use_monochrome_app_icon" - namespace: "systemui" - description: "Experiment to replace the notification icon in the status bar and shelf with the monochrome app icon, if available." - bug: "335211019" -} - -# ^^^ Prototypes for using app icons in notifications ^^^ - flag { name: "notification_expansion_optional" namespace: "systemui" diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index fff980fa8585..c424229f479b 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -353,7 +353,7 @@ flag { flag { name: "cloud_compilation_pm" is_exported: true - namespace: "package_manager_service" + namespace: "art_mainline" description: "Feature flag to enable the Cloud Compilation support on the package manager side." bug: "377474232" is_fixed_read_only: true diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 86bbd4a57a63..987e2ad768b0 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -4289,6 +4289,39 @@ public abstract class CameraMetadata<TKey> { */ public static final int SYNC_FRAME_NUMBER_UNKNOWN = -2; + // + // Enumeration values for CaptureResult#EXTENSION_NIGHT_MODE_INDICATOR + // + + /** + * <p>The camera can't accurately assess the scene's lighting to determine if a Night Mode + * Camera Extension capture would improve the photo. This can happen when the current + * camera configuration doesn't support night mode indicator detection, such as when + * the auto exposure mode is ON_AUTO_FLASH, ON_ALWAYS_FLASH, ON_AUTO_FLASH_REDEYE, or + * ON_EXTERNAL_FLASH.</p> + * @see CaptureResult#EXTENSION_NIGHT_MODE_INDICATOR + */ + @FlaggedApi(Flags.FLAG_NIGHT_MODE_INDICATOR) + public static final int EXTENSION_NIGHT_MODE_INDICATOR_UNKNOWN = 0; + + /** + * <p>The camera has detected lighting conditions that are sufficiently bright. Night + * Mode Camera Extensions is available but may not be able to optimize the camera + * settings to take a higher quality photo.</p> + * @see CaptureResult#EXTENSION_NIGHT_MODE_INDICATOR + */ + @FlaggedApi(Flags.FLAG_NIGHT_MODE_INDICATOR) + public static final int EXTENSION_NIGHT_MODE_INDICATOR_OFF = 1; + + /** + * <p>The camera has detected low-light conditions. It is recommended to use Night Mode + * Camera Extension to optimize the camera settings to take a high-quality photo in + * the dark.</p> + * @see CaptureResult#EXTENSION_NIGHT_MODE_INDICATOR + */ + @FlaggedApi(Flags.FLAG_NIGHT_MODE_INDICATOR) + public static final int EXTENSION_NIGHT_MODE_INDICATOR_ON = 2; + /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * End generated code *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/ diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index ae72ca40fc5a..bf3a072ff097 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -6016,6 +6016,38 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { public static final Key<Integer> EXTENSION_STRENGTH = new Key<Integer>("android.extension.strength", int.class); + /** + * <p>Indicates when to activate Night Mode Camera Extension for high-quality + * still captures in low-light conditions.</p> + * <p>Provides awareness to the application when the current scene can benefit from using a + * Night Mode Camera Extension to take a high-quality photo.</p> + * <p>Support for this capture result can be queried via + * {@link android.hardware.camera2.CameraCharacteristics#getAvailableCaptureResultKeys }.</p> + * <p>If the device supports this capability then it will also support + * {@link android.hardware.camera2.CameraExtensionCharacteristics#EXTENSION_NIGHT NIGHT} + * and will be available in both + * {@link android.hardware.camera2.CameraCaptureSession sessions} and + * {@link android.hardware.camera2.CameraExtensionSession sessions}.</p> + * <p>The value will be {@code UNKNOWN} in the following auto exposure modes: ON_AUTO_FLASH, + * ON_ALWAYS_FLASH, ON_AUTO_FLASH_REDEYE, or ON_EXTERNAL_FLASH.</p> + * <p><b>Possible values:</b></p> + * <ul> + * <li>{@link #EXTENSION_NIGHT_MODE_INDICATOR_UNKNOWN UNKNOWN}</li> + * <li>{@link #EXTENSION_NIGHT_MODE_INDICATOR_OFF OFF}</li> + * <li>{@link #EXTENSION_NIGHT_MODE_INDICATOR_ON ON}</li> + * </ul> + * + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * @see #EXTENSION_NIGHT_MODE_INDICATOR_UNKNOWN + * @see #EXTENSION_NIGHT_MODE_INDICATOR_OFF + * @see #EXTENSION_NIGHT_MODE_INDICATOR_ON + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_NIGHT_MODE_INDICATOR) + public static final Key<Integer> EXTENSION_NIGHT_MODE_INDICATOR = + new Key<Integer>("android.extension.nightModeIndicator", int.class); + /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * End generated code *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/ diff --git a/core/java/android/hardware/display/BrightnessInfo.java b/core/java/android/hardware/display/BrightnessInfo.java index 6a96a54d93ba..529ee91cbfdf 100644 --- a/core/java/android/hardware/display/BrightnessInfo.java +++ b/core/java/android/hardware/display/BrightnessInfo.java @@ -113,16 +113,24 @@ public final class BrightnessInfo implements Parcelable { */ public final int brightnessMaxReason; + /** + * Whether the current brightness value is overridden by the application window via + * {@link android.view.WindowManager.LayoutParams#screenBrightness}. + */ + public final boolean isBrightnessOverrideByWindow; + public BrightnessInfo(float brightness, float brightnessMinimum, float brightnessMaximum, @HighBrightnessMode int highBrightnessMode, float highBrightnessTransitionPoint, @BrightnessMaxReason int brightnessMaxReason) { this(brightness, brightness, brightnessMinimum, brightnessMaximum, highBrightnessMode, - highBrightnessTransitionPoint, brightnessMaxReason); + highBrightnessTransitionPoint, brightnessMaxReason, + false /* isBrightnessOverrideByWindow */); } public BrightnessInfo(float brightness, float adjustedBrightness, float brightnessMinimum, float brightnessMaximum, @HighBrightnessMode int highBrightnessMode, - float highBrightnessTransitionPoint, @BrightnessMaxReason int brightnessMaxReason) { + float highBrightnessTransitionPoint, @BrightnessMaxReason int brightnessMaxReason, + boolean isBrightnessOverrideByWindow) { this.brightness = brightness; this.adjustedBrightness = adjustedBrightness; this.brightnessMinimum = brightnessMinimum; @@ -130,6 +138,7 @@ public final class BrightnessInfo implements Parcelable { this.highBrightnessMode = highBrightnessMode; this.highBrightnessTransitionPoint = highBrightnessTransitionPoint; this.brightnessMaxReason = brightnessMaxReason; + this.isBrightnessOverrideByWindow = isBrightnessOverrideByWindow; } /** @@ -178,6 +187,7 @@ public final class BrightnessInfo implements Parcelable { dest.writeInt(highBrightnessMode); dest.writeFloat(highBrightnessTransitionPoint); dest.writeInt(brightnessMaxReason); + dest.writeBoolean(isBrightnessOverrideByWindow); } public static final @android.annotation.NonNull Creator<BrightnessInfo> CREATOR = @@ -201,6 +211,7 @@ public final class BrightnessInfo implements Parcelable { highBrightnessMode = source.readInt(); highBrightnessTransitionPoint = source.readFloat(); brightnessMaxReason = source.readInt(); + isBrightnessOverrideByWindow = source.readBoolean(); } } diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index a452226c81ac..28da644dd837 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -16,6 +16,7 @@ package android.hardware.display; +import static android.Manifest.permission.MANAGE_DISPLAYS; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.HdrCapabilities.HdrType; import static android.view.Display.INVALID_DISPLAY; @@ -1764,6 +1765,29 @@ public final class DisplayManager { } /** + * @return The current display topology that represents the relative positions of extended + * displays. + * + * @hide + */ + @RequiresPermission(MANAGE_DISPLAYS) + @Nullable + public DisplayTopology getDisplayTopology() { + return mGlobal.getDisplayTopology(); + } + + /** + * Set the relative positions between extended displays (display topology). + * @param topology The display topology to be set + * + * @hide + */ + @RequiresPermission(MANAGE_DISPLAYS) + public void setDisplayTopology(DisplayTopology topology) { + mGlobal.setDisplayTopology(topology); + } + + /** * Listens for changes in available display devices. */ public interface DisplayListener { diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index 644850a5c2e1..03b44f63e3b7 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -18,6 +18,7 @@ package android.hardware.display; import static android.hardware.display.DisplayManager.EventFlag; +import static android.Manifest.permission.MANAGE_DISPLAYS; import static android.view.Display.HdrCapabilities.HdrType; import android.Manifest; @@ -1285,6 +1286,31 @@ public final class DisplayManagerGlobal { } } + /** + * @see DisplayManager#getDisplayTopology + */ + @RequiresPermission(MANAGE_DISPLAYS) + @Nullable + public DisplayTopology getDisplayTopology() { + try { + return mDm.getDisplayTopology(); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + + /** + * @see DisplayManager#setDisplayTopology + */ + @RequiresPermission(MANAGE_DISPLAYS) + public void setDisplayTopology(DisplayTopology topology) { + try { + mDm.setDisplayTopology(topology); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + private final class DisplayManagerCallback extends IDisplayManagerCallback.Stub { @Override public void onDisplayEvent(int displayId, @DisplayEvent int event) { diff --git a/core/java/android/hardware/display/DisplayTopology.aidl b/core/java/android/hardware/display/DisplayTopology.aidl new file mode 100644 index 000000000000..e69b777a30de --- /dev/null +++ b/core/java/android/hardware/display/DisplayTopology.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.hardware.display; + +parcelable DisplayTopology; diff --git a/services/core/java/com/android/server/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java index fdadafeb98c9..e349b81614bc 100644 --- a/services/core/java/com/android/server/display/DisplayTopology.java +++ b/core/java/android/hardware/display/DisplayTopology.java @@ -14,25 +14,34 @@ * limitations under the License. */ -package com.android.server.display; +package android.hardware.display; -import static com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_BOTTOM; -import static com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_LEFT; -import static com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_TOP; -import static com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_RIGHT; +import static android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM; +import static android.hardware.display.DisplayTopology.TreeNode.POSITION_LEFT; +import static android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT; +import static android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP; +import android.annotation.IntDef; import android.annotation.Nullable; import android.graphics.RectF; +import android.os.Parcel; +import android.os.Parcelable; import android.util.IndentingPrintWriter; import android.util.Pair; import android.util.Slog; import android.view.Display; +import androidx.annotation.NonNull; + import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -42,24 +51,59 @@ import java.util.Queue; /** * Represents the relative placement of extended displays. * Does not support concurrent calls, so a lock should be held when calling into this class. + * + * @hide */ -class DisplayTopology { +public final class DisplayTopology implements Parcelable { private static final String TAG = "DisplayTopology"; private static final float EPSILON = 0.0001f; + @android.annotation.NonNull + public static final Creator<DisplayTopology> CREATOR = + new Creator<>() { + @Override + public DisplayTopology createFromParcel(Parcel source) { + return new DisplayTopology(source); + } + + @Override + public DisplayTopology[] newArray(int size) { + return new DisplayTopology[size]; + } + }; + /** * The topology tree */ @Nullable - @VisibleForTesting - TreeNode mRoot; + private TreeNode mRoot; /** * The logical display ID of the primary display that will show certain UI elements. * This is not necessarily the same as the default display. */ + private int mPrimaryDisplayId = Display.INVALID_DISPLAY; + + public DisplayTopology() {} + @VisibleForTesting - int mPrimaryDisplayId = Display.INVALID_DISPLAY; + public DisplayTopology(TreeNode root, int primaryDisplayId) { + mRoot = root; + mPrimaryDisplayId = primaryDisplayId; + } + + public DisplayTopology(Parcel source) { + this(source.readTypedObject(TreeNode.CREATOR), source.readInt()); + } + + @Nullable + public TreeNode getRoot() { + return mRoot; + } + + public int getPrimaryDisplayId() { + return mPrimaryDisplayId; + } /** * Add a display to the topology. @@ -69,7 +113,7 @@ class DisplayTopology { * @param width The width of the display * @param height The height of the display */ - void addDisplay(int displayId, float width, float height) { + public void addDisplay(int displayId, float width, float height) { addDisplay(displayId, width, height, /* shouldLog= */ true); } @@ -79,7 +123,7 @@ class DisplayTopology { * one by one. * @param displayId The logical display ID */ - void removeDisplay(int displayId) { + public void removeDisplay(int displayId) { if (findDisplay(displayId, mRoot) == null) { return; } @@ -106,11 +150,22 @@ class DisplayTopology { } } + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeTypedObject(mRoot, flags); + dest.writeInt(mPrimaryDisplayId); + } + /** * Print the object's state and debug information into the given stream. * @param pw The stream to dump information to. */ - void dump(PrintWriter pw) { + public void dump(PrintWriter pw) { pw.println("DisplayTopology:"); pw.println("--------------------"); IndentingPrintWriter ipw = new IndentingPrintWriter(pw); @@ -126,13 +181,21 @@ class DisplayTopology { } } + @Override + public String toString() { + StringWriter out = new StringWriter(); + PrintWriter writer = new PrintWriter(out); + dump(writer); + return out.toString(); + } + private void addDisplay(int displayId, float width, float height, boolean shouldLog) { if (findDisplay(displayId, mRoot) != null) { throw new IllegalArgumentException( "DisplayTopology: attempting to add a display that already exists"); } if (mRoot == null) { - mRoot = new TreeNode(displayId, width, height, /* position= */ null, /* offset= */ 0); + mRoot = new TreeNode(displayId, width, height, /* position= */ 0, /* offset= */ 0); mPrimaryDisplayId = displayId; if (shouldLog) { Slog.i(TAG, "First display added: " + mRoot); @@ -241,7 +304,7 @@ class DisplayTopology { * Update the topology to remove any overlaps between displays. */ @VisibleForTesting - void normalize() { + public void normalize() { if (mRoot == null) { return; } @@ -341,6 +404,8 @@ class DisplayTopology { case POSITION_RIGHT -> floatEquals(parentBounds.right, childBounds.left); case POSITION_TOP -> floatEquals(parentBounds.top, childBounds.bottom); case POSITION_BOTTOM -> floatEquals(parentBounds.bottom, childBounds.top); + default -> throw new IllegalStateException( + "Unexpected value: " + targetDisplay.mPosition); }; // Check that the offset is within bounds areTouching &= switch (targetDisplay.mPosition) { @@ -350,6 +415,8 @@ class DisplayTopology { case POSITION_TOP, POSITION_BOTTOM -> childBounds.right + EPSILON >= parentBounds.left && childBounds.left <= parentBounds.right + EPSILON; + default -> throw new IllegalStateException( + "Unexpected value: " + targetDisplay.mPosition); }; if (!areTouching) { @@ -379,36 +446,56 @@ class DisplayTopology { * @param b second float to compare * @return whether the two values are within a small enough tolerance value */ - public static boolean floatEquals(float a, float b) { - return a == b || Float.isNaN(a) && Float.isNaN(b) || Math.abs(a - b) < EPSILON; + private static boolean floatEquals(float a, float b) { + return a == b || (Float.isNaN(a) && Float.isNaN(b)) || Math.abs(a - b) < EPSILON; } - @VisibleForTesting - static class TreeNode { + public static final class TreeNode implements Parcelable { + public static final int POSITION_LEFT = 0; + public static final int POSITION_TOP = 1; + public static final int POSITION_RIGHT = 2; + public static final int POSITION_BOTTOM = 3; + + @IntDef(prefix = { "POSITION_" }, value = { + POSITION_LEFT, POSITION_TOP, POSITION_RIGHT, POSITION_BOTTOM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Position{} + + @android.annotation.NonNull + public static final Creator<TreeNode> CREATOR = + new Creator<>() { + @Override + public TreeNode createFromParcel(Parcel source) { + return new TreeNode(source); + } + + @Override + public TreeNode[] newArray(int size) { + return new TreeNode[size]; + } + }; /** * The logical display ID */ - @VisibleForTesting - final int mDisplayId; + private final int mDisplayId; /** * The width of the display in density-independent pixels (dp). */ - @VisibleForTesting - float mWidth; + private final float mWidth; /** * The height of the display in density-independent pixels (dp). */ - @VisibleForTesting - float mHeight; + private final float mHeight; /** * The position of this display relative to its parent. */ - @VisibleForTesting - Position mPosition; + @Position + private int mPosition; /** * The distance from the top edge of the parent display to the top edge of this display (in @@ -416,13 +503,13 @@ class DisplayTopology { * to the left edge of this display (in case of POSITION_TOP or POSITION_BOTTOM). The unit * used is density-independent pixels (dp). */ - @VisibleForTesting - float mOffset; + private float mOffset; - @VisibleForTesting - final List<TreeNode> mChildren = new ArrayList<>(); + private final List<TreeNode> mChildren = new ArrayList<>(); - TreeNode(int displayId, float width, float height, Position position, float offset) { + @VisibleForTesting + public TreeNode(int displayId, float width, float height, @Position int position, + float offset) { mDisplayId = displayId; mWidth = width; mHeight = height; @@ -430,11 +517,76 @@ class DisplayTopology { mOffset = offset; } + public TreeNode(Parcel source) { + this(source.readInt(), source.readFloat(), source.readFloat(), source.readInt(), + source.readFloat()); + source.readTypedList(mChildren, CREATOR); + } + + public int getDisplayId() { + return mDisplayId; + } + + public float getWidth() { + return mWidth; + } + + public float getHeight() { + return mHeight; + } + + public int getPosition() { + return mPosition; + } + + public float getOffset() { + return mOffset; + } + + public List<TreeNode> getChildren() { + return Collections.unmodifiableList(mChildren); + } + + @Override + public String toString() { + return "Display {id=" + mDisplayId + ", width=" + mWidth + ", height=" + mHeight + + ", position=" + positionToString(mPosition) + ", offset=" + mOffset + "}"; + } + + /** + * @param position The position + * @return The string representation + */ + public static String positionToString(@Position int position) { + return switch (position) { + case POSITION_LEFT -> "left"; + case POSITION_TOP -> "top"; + case POSITION_RIGHT -> "right"; + case POSITION_BOTTOM -> "bottom"; + default -> throw new IllegalStateException("Unexpected value: " + position); + }; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mDisplayId); + dest.writeFloat(mWidth); + dest.writeFloat(mHeight); + dest.writeInt(mPosition); + dest.writeFloat(mOffset); + dest.writeTypedList(mChildren); + } + /** * Print the object's state and debug information into the given stream. * @param ipw The stream to dump information to. */ - void dump(IndentingPrintWriter ipw) { + public void dump(IndentingPrintWriter ipw) { ipw.println(this); ipw.increaseIndent(); for (TreeNode child : mChildren) { @@ -443,15 +595,12 @@ class DisplayTopology { ipw.decreaseIndent(); } - @Override - public String toString() { - return "Display {id=" + mDisplayId + ", width=" + mWidth + ", height=" + mHeight - + ", position=" + mPosition + ", offset=" + mOffset + "}"; - } - + /** + * @param child The child to add + */ @VisibleForTesting - enum Position { - POSITION_LEFT, POSITION_TOP, POSITION_RIGHT, POSITION_BOTTOM + public void addChild(TreeNode child) { + mChildren.add(child); } } } diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl index b612bca5671e..4fbdf7f5afc8 100644 --- a/core/java/android/hardware/display/IDisplayManager.aidl +++ b/core/java/android/hardware/display/IDisplayManager.aidl @@ -23,6 +23,7 @@ import android.hardware.display.BrightnessConfiguration; import android.hardware.display.BrightnessInfo; import android.hardware.display.Curve; import android.hardware.graphics.common.DisplayDecorationSupport; +import android.hardware.display.DisplayTopology; import android.hardware.display.HdrConversionMode; import android.hardware.display.IDisplayManagerCallback; import android.hardware.display.IVirtualDisplayCallback; @@ -254,4 +255,13 @@ interface IDisplayManager { // Get the default doze brightness @EnforcePermission("CONTROL_DISPLAY_BRIGHTNESS") float getDefaultDozeBrightness(int displayId); + + // Get the display topology + @EnforcePermission("MANAGE_DISPLAYS") + @nullable + DisplayTopology getDisplayTopology(); + + // Set the display topology + @EnforcePermission("MANAGE_DISPLAYS") + void setDisplayTopology(in DisplayTopology topology); } diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 1b96224f03da..3284761eb273 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -276,9 +276,9 @@ interface IInputManager { @PermissionManuallyEnforced @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.MANAGE_KEY_GESTURES)") - void removeAllCustomInputGestures(int userId); + void removeAllCustomInputGestures(int userId, int tag); - AidlInputGestureData[] getCustomInputGestures(int userId); + AidlInputGestureData[] getCustomInputGestures(int userId, int tag); AidlInputGestureData[] getAppLaunchBookmarks(); } diff --git a/core/java/android/hardware/input/InputGestureData.java b/core/java/android/hardware/input/InputGestureData.java index ee0a2a9cf88c..f41550f6061e 100644 --- a/core/java/android/hardware/input/InputGestureData.java +++ b/core/java/android/hardware/input/InputGestureData.java @@ -296,4 +296,35 @@ public final class InputGestureData { public record Action(@KeyGestureEvent.KeyGestureType int keyGestureType, @Nullable AppLaunchData appLaunchData) { } + + /** Filter definition for InputGestureData */ + public enum Filter { + KEY(AidlInputGestureData.Trigger.Tag.key), + TOUCHPAD(AidlInputGestureData.Trigger.Tag.touchpadGesture); + + @AidlInputGestureData.Trigger.Tag + private final int mTag; + + Filter(@AidlInputGestureData.Trigger.Tag int tag) { + mTag = tag; + } + + @Nullable + public static Filter of(@AidlInputGestureData.Trigger.Tag int tag) { + return switch (tag) { + case AidlInputGestureData.Trigger.Tag.key -> KEY; + case AidlInputGestureData.Trigger.Tag.touchpadGesture -> TOUCHPAD; + default -> null; + }; + } + + @AidlInputGestureData.Trigger.Tag + public int getTag() { + return mTag; + } + + public boolean matches(@NonNull InputGestureData inputGestureData) { + return mTag == inputGestureData.mInputGestureData.trigger.getTag(); + } + } } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 9050ae235ce7..f8241925dff0 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1526,16 +1526,20 @@ public final class InputManager { /** Removes all custom input gestures * + * @param filter for removing all gestures of a category. If {@code null}, all custom input + * gestures will be removed + * * @hide */ @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) @UserHandleAware - public void removeAllCustomInputGestures() { + public void removeAllCustomInputGestures(@Nullable InputGestureData.Filter filter) { if (!enableCustomizableInputGestures()) { return; } try { - mIm.removeAllCustomInputGestures(mContext.getUserId()); + mIm.removeAllCustomInputGestures(mContext.getUserId(), + filter == null ? -1 : filter.getTag()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1543,16 +1547,20 @@ public final class InputManager { /** Get all custom input gestures * + * @param filter for fetching all gestures of a category. If {@code null}, then will return + * all custom input gestures + * * @hide */ @UserHandleAware - public List<InputGestureData> getCustomInputGestures() { + public List<InputGestureData> getCustomInputGestures(@Nullable InputGestureData.Filter filter) { List<InputGestureData> result = new ArrayList<>(); if (!enableCustomizableInputGestures()) { return result; } try { - for (AidlInputGestureData data : mIm.getCustomInputGestures(mContext.getUserId())) { + for (AidlInputGestureData data : mIm.getCustomInputGestures(mContext.getUserId(), + filter == null ? -1 : filter.getTag())) { result.add(new InputGestureData(data)); } } catch (RemoteException e) { diff --git a/core/java/android/hardware/location/ContextHubInfo.java b/core/java/android/hardware/location/ContextHubInfo.java index 858ec23ebed8..af715e485b73 100644 --- a/core/java/android/hardware/location/ContextHubInfo.java +++ b/core/java/android/hardware/location/ContextHubInfo.java @@ -15,7 +15,6 @@ */ package android.hardware.location; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java index 6284e7061b88..494bfc926384 100644 --- a/core/java/android/hardware/location/ContextHubManager.java +++ b/core/java/android/hardware/location/ContextHubManager.java @@ -18,6 +18,7 @@ package android.hardware.location; import static java.util.Objects.requireNonNull; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -31,7 +32,6 @@ import android.app.ActivityThread; import android.app.PendingIntent; import android.chre.flags.Flags; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageManager; import android.hardware.contexthub.ErrorCode; import android.os.Handler; @@ -484,15 +484,33 @@ public final class ContextHubManager { } } - /** - * Helper function to generate a stub for a query transaction callback. - * - * @param transaction the transaction to unblock when complete - * - * @return the callback - * - * @hide - */ + /** + * Returns the list of HubInfo objects describing the available hubs (including ContextHub and + * VendorHub). This method is primarily used for debugging purposes as most clients care about + * endpoints and services more than hubs. + * + * @return the list of HubInfo objects + * @see HubInfo + * @hide + */ + @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) + @NonNull + @FlaggedApi(Flags.FLAG_OFFLOAD_API) + public List<HubInfo> getHubs() { + try { + return mService.getHubs(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Helper function to generate a stub for a query transaction callback. + * + * @param transaction the transaction to unblock when complete + * @return the callback + * @hide + */ private IContextHubTransactionCallback createQueryCallback( ContextHubTransaction<List<NanoAppState>> transaction) { return new IContextHubTransactionCallback.Stub() { diff --git a/core/java/android/hardware/location/HubInfo.aidl b/core/java/android/hardware/location/HubInfo.aidl new file mode 100644 index 000000000000..25b5b0aa1222 --- /dev/null +++ b/core/java/android/hardware/location/HubInfo.aidl @@ -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. + */ + +package android.hardware.location; + +/** @hide */ +parcelable HubInfo; diff --git a/core/java/android/hardware/location/HubInfo.java b/core/java/android/hardware/location/HubInfo.java new file mode 100644 index 000000000000..f7de1279672c --- /dev/null +++ b/core/java/android/hardware/location/HubInfo.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.hardware.location; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.chre.flags.Flags; +import android.os.BadParcelableException; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Union type for {@link ContextHubInfo} and {@link VendorHubInfo} + * + * @hide + */ +@FlaggedApi(Flags.FLAG_OFFLOAD_API) +public final class HubInfo implements Parcelable { + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {TYPE_CONTEXT_HUB, TYPE_VENDOR_HUB}) + private @interface HubType {} + + public static final int TYPE_CONTEXT_HUB = 0; + public static final int TYPE_VENDOR_HUB = 1; + + private final long mId; + @HubType private final int mType; + @Nullable private final ContextHubInfo mContextHubInfo; + @Nullable private final VendorHubInfo mVendorHubInfo; + + /** @hide */ + public HubInfo(long id, @NonNull ContextHubInfo contextHubInfo) { + mId = id; + mType = TYPE_CONTEXT_HUB; + mContextHubInfo = contextHubInfo; + mVendorHubInfo = null; + } + + /** @hide */ + public HubInfo(long id, @NonNull VendorHubInfo vendorHubInfo) { + mId = id; + mType = TYPE_VENDOR_HUB; + mContextHubInfo = null; + mVendorHubInfo = vendorHubInfo; + } + + private HubInfo(Parcel in) { + mId = in.readLong(); + mType = in.readInt(); + + switch (mType) { + case TYPE_CONTEXT_HUB: + mContextHubInfo = ContextHubInfo.CREATOR.createFromParcel(in); + mVendorHubInfo = null; + break; + case TYPE_VENDOR_HUB: + mVendorHubInfo = VendorHubInfo.CREATOR.createFromParcel(in); + mContextHubInfo = null; + break; + default: + throw new BadParcelableException("Parcelable has invalid type"); + } + } + + /** Get the hub unique identifier */ + public long getId() { + return mId; + } + + /** Get the hub type. The type can be {@link TYPE_CONTEXT_HUB} or {@link TYPE_VENDOR_HUB} */ + public int getType() { + return mType; + } + + /** Get the {@link ContextHubInfo} object, null if type is not {@link TYPE_CONTEXT_HUB} */ + @Nullable + public ContextHubInfo getContextHubInfo() { + return mContextHubInfo; + } + + /** Parcel implementation details */ + public int describeContents() { + if (mType == TYPE_CONTEXT_HUB && mContextHubInfo != null) { + return mContextHubInfo.describeContents(); + } + if (mType == TYPE_VENDOR_HUB && mVendorHubInfo != null) { + return mVendorHubInfo.describeContents(); + } + return 0; + } + + /** Parcel implementation details */ + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeLong(mId); + out.writeInt(mType); + + if (mType == TYPE_CONTEXT_HUB && mContextHubInfo != null) { + mContextHubInfo.writeToParcel(out, flags); + } + + if (mType == TYPE_VENDOR_HUB && mVendorHubInfo != null) { + mVendorHubInfo.writeToParcel(out, flags); + } + } + + @NonNull + @Override + public String toString() { + StringBuilder out = new StringBuilder(); + out.append("HubInfo ID: 0x"); + out.append(Long.toHexString(mId)); + out.append("\n"); + if (mType == TYPE_CONTEXT_HUB && mContextHubInfo != null) { + out.append(" ContextHubDetails: "); + out.append(mContextHubInfo); + } + if (mType == TYPE_VENDOR_HUB && mVendorHubInfo != null) { + out.append(" VendorHubDetails: "); + out.append(mVendorHubInfo); + } + return out.toString(); + } + + public static final @NonNull Creator<HubInfo> CREATOR = + new Creator<>() { + public HubInfo createFromParcel(Parcel in) { + return new HubInfo(in); + } + + public HubInfo[] newArray(int size) { + return new HubInfo[size]; + } + }; +} diff --git a/core/java/android/hardware/location/IContextHubService.aidl b/core/java/android/hardware/location/IContextHubService.aidl index 11f3046150d3..b0cc763dc8fd 100644 --- a/core/java/android/hardware/location/IContextHubService.aidl +++ b/core/java/android/hardware/location/IContextHubService.aidl @@ -18,6 +18,7 @@ package android.hardware.location; // Declare any non-default types here with import statements import android.app.PendingIntent; +import android.hardware.location.HubInfo; import android.hardware.location.ContextHubInfo; import android.hardware.location.ContextHubMessage; import android.hardware.location.NanoApp; @@ -82,6 +83,10 @@ interface IContextHubService { @EnforcePermission("ACCESS_CONTEXT_HUB") List<ContextHubInfo> getContextHubs(); + // Returns a list of HubInfo objects of available hubs (including ContextHub and VendorHub) + @EnforcePermission("ACCESS_CONTEXT_HUB") + List<HubInfo> getHubs(); + // Loads a nanoapp at the specified hub (new API) @EnforcePermission("ACCESS_CONTEXT_HUB") void loadNanoAppOnHub( diff --git a/core/java/android/hardware/location/VendorHubInfo.aidl b/core/java/android/hardware/location/VendorHubInfo.aidl new file mode 100644 index 000000000000..a7936acbb654 --- /dev/null +++ b/core/java/android/hardware/location/VendorHubInfo.aidl @@ -0,0 +1,20 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.location; + +/** @hide */ +parcelable VendorHubInfo;
\ No newline at end of file diff --git a/core/java/android/hardware/location/VendorHubInfo.java b/core/java/android/hardware/location/VendorHubInfo.java new file mode 100644 index 000000000000..26772b18176f --- /dev/null +++ b/core/java/android/hardware/location/VendorHubInfo.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.hardware.location; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.chre.flags.Flags; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.ParcelableHolder; + +/** + * Information about a VendorHub. VendorHub is similar to ContextHub, but it does not run the + * Context Hub Runtime Environment (or nano apps). It provides a unified endpoint messaging API + * through the ContextHub V4 HAL. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_OFFLOAD_API) +public final class VendorHubInfo implements Parcelable { + private final String mName; + private final int mVersion; + private final ParcelableHolder mExtendedInfo; + + /** @hide */ + public VendorHubInfo(android.hardware.contexthub.VendorHubInfo halHubInfo) { + mName = halHubInfo.name; + mVersion = halHubInfo.version; + mExtendedInfo = halHubInfo.extendedInfo; + } + + private VendorHubInfo(Parcel in) { + mName = in.readString(); + mVersion = in.readInt(); + mExtendedInfo = ParcelableHolder.CREATOR.createFromParcel(in); + } + + /** Get the hub name */ + @NonNull + public String getName() { + return mName; + } + + /** Get the hub version */ + public int getVersion() { + return mVersion; + } + + /** Parcel implementation details */ + public int describeContents() { + return mExtendedInfo.describeContents(); + } + + /** Parcel implementation details */ + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeString(mName); + out.writeInt(mVersion); + mExtendedInfo.writeToParcel(out, flags); + } + + @NonNull + @Override + public String toString() { + StringBuilder out = new StringBuilder(); + out.append("VendorHub Name : "); + out.append(mName); + out.append(", Version : "); + out.append(mVersion); + return out.toString(); + } + + public static final @NonNull Creator<VendorHubInfo> CREATOR = + new Creator<>() { + public VendorHubInfo createFromParcel(Parcel in) { + return new VendorHubInfo(in); + } + + public VendorHubInfo[] newArray(int size) { + return new VendorHubInfo[size]; + } + }; +} diff --git a/core/java/android/os/IVibratorManagerService.aidl b/core/java/android/os/IVibratorManagerService.aidl index 6aa9852314df..ecb5e6f1b29a 100644 --- a/core/java/android/os/IVibratorManagerService.aidl +++ b/core/java/android/os/IVibratorManagerService.aidl @@ -17,13 +17,17 @@ package android.os; import android.os.CombinedVibration; +import android.os.ICancellationSignal; import android.os.IVibratorStateListener; import android.os.VibrationAttributes; import android.os.VibratorInfo; +import android.os.vibrator.IVibrationSession; +import android.os.vibrator.IVibrationSessionCallback; /** {@hide} */ interface IVibratorManagerService { int[] getVibratorIds(); + int getCapabilities(); VibratorInfo getVibratorInfo(int vibratorId); @EnforcePermission("ACCESS_VIBRATOR_STATE") boolean isVibrating(int vibratorId); @@ -50,4 +54,9 @@ interface IVibratorManagerService { oneway void performHapticFeedbackForInputDevice(int uid, int deviceId, String opPkg, int constant, int inputDeviceId, int inputSource, String reason, int flags, int privFlags); + + @EnforcePermission(allOf={"VIBRATE", "VIBRATE_VENDOR_EFFECTS", "START_VIBRATION_SESSIONS"}) + ICancellationSignal startVendorVibrationSession(int uid, int deviceId, String opPkg, + in int[] vibratorIds, in VibrationAttributes attributes, String reason, + in IVibrationSessionCallback callback); } diff --git a/core/java/android/os/SystemVibrator.java b/core/java/android/os/SystemVibrator.java index 011a3ee91ada..c3cddf32f063 100644 --- a/core/java/android/os/SystemVibrator.java +++ b/core/java/android/os/SystemVibrator.java @@ -18,8 +18,11 @@ package android.os; import android.annotation.CallbackExecutor; import android.annotation.NonNull; +import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.hardware.vibrator.IVibratorManager; +import android.os.vibrator.VendorVibrationSession; import android.os.vibrator.VibratorInfoFactory; import android.util.ArrayMap; import android.util.Log; @@ -53,6 +56,7 @@ public class SystemVibrator extends Vibrator { private final Object mLock = new Object(); @GuardedBy("mLock") private VibratorInfo mVibratorInfo; + private int[] mVibratorIds; @UnsupportedAppUsage public SystemVibrator(Context context) { @@ -71,7 +75,11 @@ public class SystemVibrator extends Vibrator { Log.w(TAG, "Failed to retrieve vibrator info; no vibrator manager."); return VibratorInfo.EMPTY_VIBRATOR_INFO; } - int[] vibratorIds = mVibratorManager.getVibratorIds(); + int[] vibratorIds = getVibratorIds(); + if (vibratorIds == null) { + Log.w(TAG, "Failed to retrieve vibrator info; error retrieving vibrator ids."); + return VibratorInfo.EMPTY_VIBRATOR_INFO; + } if (vibratorIds.length == 0) { // It is known that the device has no vibrator, so cache and return info that // reflects the lack of support for effects/primitives. @@ -95,20 +103,22 @@ public class SystemVibrator extends Vibrator { @Override public boolean hasVibrator() { - if (mVibratorManager == null) { + int[] vibratorIds = getVibratorIds(); + if (vibratorIds == null) { Log.w(TAG, "Failed to check if vibrator exists; no vibrator manager."); return false; } - return mVibratorManager.getVibratorIds().length > 0; + return vibratorIds.length > 0; } @Override public boolean isVibrating() { - if (mVibratorManager == null) { + int[] vibratorIds = getVibratorIds(); + if (vibratorIds == null) { Log.w(TAG, "Failed to vibrate; no vibrator manager."); return false; } - for (int vibratorId : mVibratorManager.getVibratorIds()) { + for (int vibratorId : vibratorIds) { if (mVibratorManager.getVibrator(vibratorId).isVibrating()) { return true; } @@ -136,6 +146,11 @@ public class SystemVibrator extends Vibrator { Log.w(TAG, "Failed to add vibrate state listener; no vibrator manager."); return; } + int[] vibratorIds = getVibratorIds(); + if (vibratorIds == null) { + Log.w(TAG, "Failed to add vibrate state listener; error retrieving vibrator ids."); + return; + } MultiVibratorStateListener delegate = null; try { synchronized (mRegisteredListeners) { @@ -145,7 +160,7 @@ public class SystemVibrator extends Vibrator { return; } delegate = new MultiVibratorStateListener(executor, listener); - delegate.register(mVibratorManager); + delegate.register(mVibratorManager, vibratorIds); mRegisteredListeners.put(listener, delegate); delegate = null; } @@ -184,6 +199,11 @@ public class SystemVibrator extends Vibrator { } @Override + public boolean areVendorSessionsSupported() { + return mVibratorManager.hasCapabilities(IVibratorManager.CAP_START_SESSIONS); + } + + @Override public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId, VibrationEffect effect, VibrationAttributes attrs) { if (mVibratorManager == null) { @@ -243,6 +263,41 @@ public class SystemVibrator extends Vibrator { mVibratorManager.cancel(usageFilter); } + @Override + public void startVendorSession(@NonNull VibrationAttributes attrs, @Nullable String reason, + @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor, + @NonNull VendorVibrationSession.Callback callback) { + if (mVibratorManager == null) { + Log.w(TAG, "Failed to start vibration session; no vibrator manager."); + executor.execute( + () -> callback.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR)); + return; + } + int[] vibratorIds = getVibratorIds(); + if (vibratorIds == null) { + Log.w(TAG, "Failed to start vibration session; error retrieving vibrator ids."); + executor.execute( + () -> callback.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR)); + return; + } + mVibratorManager.startVendorSession(vibratorIds, attrs, reason, cancellationSignal, + executor, callback); + } + + @Nullable + private int[] getVibratorIds() { + synchronized (mLock) { + if (mVibratorIds != null) { + return mVibratorIds; + } + if (mVibratorManager == null) { + Log.w(TAG, "Failed to retrieve vibrator ids; no vibrator manager."); + return null; + } + return mVibratorIds = mVibratorManager.getVibratorIds(); + } + } + /** * Tries to unregister individual {@link android.os.Vibrator.OnVibratorStateChangedListener} * that were left registered to vibrators after failures to register them to all vibrators. @@ -319,8 +374,7 @@ public class SystemVibrator extends Vibrator { } /** Registers a listener to all individual vibrators in {@link VibratorManager}. */ - public void register(VibratorManager vibratorManager) { - int[] vibratorIds = vibratorManager.getVibratorIds(); + public void register(VibratorManager vibratorManager, @NonNull int[] vibratorIds) { synchronized (mLock) { for (int i = 0; i < vibratorIds.length; i++) { int vibratorId = vibratorIds[i]; diff --git a/core/java/android/os/SystemVibratorManager.java b/core/java/android/os/SystemVibratorManager.java index a5697fb0e8a8..f9935d2870b0 100644 --- a/core/java/android/os/SystemVibratorManager.java +++ b/core/java/android/os/SystemVibratorManager.java @@ -22,6 +22,10 @@ import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.hardware.vibrator.IVibratorManager; +import android.os.vibrator.IVibrationSession; +import android.os.vibrator.IVibrationSessionCallback; +import android.os.vibrator.VendorVibrationSession; import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; @@ -47,6 +51,8 @@ public class SystemVibratorManager extends VibratorManager { @GuardedBy("mLock") private int[] mVibratorIds; @GuardedBy("mLock") + private int mCapabilities; + @GuardedBy("mLock") private final SparseArray<Vibrator> mVibrators = new SparseArray<>(); @GuardedBy("mLock") @@ -84,6 +90,11 @@ public class SystemVibratorManager extends VibratorManager { } } + @Override + public boolean hasCapabilities(int capabilities) { + return (getCapabilities() & capabilities) == capabilities; + } + @NonNull @Override public Vibrator getVibrator(int vibratorId) { @@ -173,7 +184,7 @@ public class SystemVibratorManager extends VibratorManager { int inputSource, String reason, int flags, int privFlags) { if (mService == null) { Log.w(TAG, "Failed to perform haptic feedback for input device;" - + " no vibrator manager service."); + + " no vibrator manager service."); return; } Trace.traceBegin(TRACE_TAG_VIBRATOR, "performHapticFeedbackForInputDevice"); @@ -197,6 +208,50 @@ public class SystemVibratorManager extends VibratorManager { cancelVibration(usageFilter); } + @Override + public void startVendorSession(@NonNull int[] vibratorIds, @NonNull VibrationAttributes attrs, + @Nullable String reason, @Nullable CancellationSignal cancellationSignal, + @NonNull Executor executor, @NonNull VendorVibrationSession.Callback callback) { + Objects.requireNonNull(vibratorIds); + VendorVibrationSessionCallbackDelegate callbackDelegate = + new VendorVibrationSessionCallbackDelegate(executor, callback); + if (mService == null) { + Log.w(TAG, "Failed to start vibration session; no vibrator manager service."); + callbackDelegate.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR); + return; + } + try { + ICancellationSignal remoteCancellationSignal = mService.startVendorVibrationSession( + mUid, mContext.getDeviceId(), mPackageName, vibratorIds, attrs, reason, + callbackDelegate); + if (cancellationSignal != null) { + cancellationSignal.setRemote(remoteCancellationSignal); + } + } catch (RemoteException e) { + Log.w(TAG, "Failed to start vibration session.", e); + callbackDelegate.onFinished(VendorVibrationSession.STATUS_UNKNOWN_ERROR); + } + } + + private int getCapabilities() { + synchronized (mLock) { + if (mCapabilities != 0) { + return mCapabilities; + } + try { + if (mService == null) { + Log.w(TAG, "Failed to retrieve vibrator manager capabilities;" + + " no vibrator manager service."); + } else { + return mCapabilities = mService.getCapabilities(); + } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return 0; + } + } + private void cancelVibration(int usageFilter) { if (mService == null) { Log.w(TAG, "Failed to cancel vibration; no vibrator manager service."); @@ -228,12 +283,45 @@ public class SystemVibratorManager extends VibratorManager { } } + /** Callback for vendor vibration sessions. */ + private static class VendorVibrationSessionCallbackDelegate extends + IVibrationSessionCallback.Stub { + private final Executor mExecutor; + private final VendorVibrationSession.Callback mCallback; + + VendorVibrationSessionCallbackDelegate( + @NonNull Executor executor, + @NonNull VendorVibrationSession.Callback callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onStarted(IVibrationSession session) { + mExecutor.execute(() -> mCallback.onStarted(new VendorVibrationSession(session))); + } + + @Override + public void onFinishing() { + mExecutor.execute(() -> mCallback.onFinishing()); + } + + @Override + public void onFinished(int status) { + mExecutor.execute(() -> mCallback.onFinished(status)); + } + } + /** Controls vibrations on a single vibrator. */ private final class SingleVibrator extends Vibrator { private final VibratorInfo mVibratorInfo; + private final int[] mVibratorId; SingleVibrator(@NonNull VibratorInfo vibratorInfo) { mVibratorInfo = vibratorInfo; + mVibratorId = new int[]{mVibratorInfo.getId()}; } @Override @@ -252,6 +340,11 @@ public class SystemVibratorManager extends VibratorManager { } @Override + public boolean areVendorSessionsSupported() { + return SystemVibratorManager.this.hasCapabilities(IVibratorManager.CAP_START_SESSIONS); + } + + @Override public boolean setAlwaysOnEffect(int uid, String opPkg, int alwaysOnId, @Nullable VibrationEffect effect, @Nullable VibrationAttributes attrs) { CombinedVibration combined = CombinedVibration.startParallel() @@ -369,5 +462,13 @@ public class SystemVibratorManager extends VibratorManager { } } } + + @Override + public void startVendorSession(@NonNull VibrationAttributes attrs, String reason, + @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor, + @NonNull VendorVibrationSession.Callback callback) { + SystemVibratorManager.this.startVendorSession(mVibratorId, attrs, reason, + cancellationSignal, executor, callback); + } } } diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index c4c4580bf0a8..53f8a9267499 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -33,6 +33,7 @@ import android.content.res.Resources; import android.hardware.vibrator.IVibrator; import android.media.AudioAttributes; import android.os.vibrator.Flags; +import android.os.vibrator.VendorVibrationSession; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibratorFrequencyProfile; import android.os.vibrator.VibratorFrequencyProfileLegacy; @@ -247,6 +248,34 @@ public abstract class Vibrator { } /** + * Check whether the vibrator has support for vendor-specific effects. + * + * <p>Vendor vibration effects can be created via {@link VibrationEffect#createVendorEffect}. + * + * @return True if the hardware can play vendor-specific vibration effects, false otherwise. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public boolean areVendorEffectsSupported() { + return getInfo().hasCapability(IVibrator.CAP_PERFORM_VENDOR_EFFECTS); + } + + /** + * Check whether the vibrator has support for vendor-specific vibration sessions. + * + * <p>Vendor vibration sessions can be started via {@link #startVendorSession}. + * + * @return True if the hardware can play vendor-specific vibration sessions, false otherwise. + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public boolean areVendorSessionsSupported() { + return false; + } + + /** * Check whether the vibrator can be controlled by an external service with the * {@link IExternalVibratorService}. * @@ -922,4 +951,44 @@ public abstract class Vibrator { @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void removeVibratorStateListener(@NonNull OnVibratorStateChangedListener listener) { } + + /** + * Starts a vibration session in this vibrator. + * + * <p>The session will start asynchronously once the vibrator control can be acquired. Once it's + * started the {@link VendorVibrationSession} will be provided to the callback. This session + * should be used to play vibrations until the session is ended or canceled. + * + * <p>The vendor app will have exclusive control over the vibrator during this session. This + * control can be revoked by the vibrator service, which will be notified to the same session + * callback with the {@link VendorVibrationSession#STATUS_CANCELED}. + * + * <p>The {@link VibrationAttributes} will be used to decide the priority of the vendor + * vibrations that will be performed in this session. All vibrations within this session will + * apply the same attributes. + * + * @param attrs The {@link VibrationAttributes} corresponding to the vibrations that will be + * performed in the session. This will be used to decide the priority of this + * session against other system vibrations. + * @param reason The description for this session, used for debugging purposes. + * @param cancellationSignal A signal to cancel the session before it starts. + * @param executor The executor for the session callbacks. + * @param callback The {@link VendorVibrationSession.Callback} for the started session. + * + * @see VendorVibrationSession + * @hide + */ + @SystemApi + @FlaggedApi(Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @RequiresPermission(allOf = { + android.Manifest.permission.VIBRATE, + android.Manifest.permission.VIBRATE_VENDOR_EFFECTS, + android.Manifest.permission.START_VIBRATION_SESSIONS, + }) + public void startVendorSession(@NonNull VibrationAttributes attrs, @Nullable String reason, + @Nullable CancellationSignal cancellationSignal, @NonNull Executor executor, + @NonNull VendorVibrationSession.Callback callback) { + Log.w(TAG, "startVendorSession is not supported"); + executor.execute(() -> callback.onFinished(VendorVibrationSession.STATUS_UNSUPPORTED)); + } } diff --git a/core/java/android/os/VibratorManager.java b/core/java/android/os/VibratorManager.java index 0428876891f9..0072bc22ad8f 100644 --- a/core/java/android/os/VibratorManager.java +++ b/core/java/android/os/VibratorManager.java @@ -22,9 +22,12 @@ import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.app.ActivityThread; import android.content.Context; +import android.os.vibrator.VendorVibrationSession; import android.util.Log; import android.view.HapticFeedbackConstants; +import java.util.concurrent.Executor; + /** * Provides access to all vibrators from the device, as well as the ability to run them * in a synchronized fashion. @@ -62,6 +65,14 @@ public abstract class VibratorManager { public abstract int[] getVibratorIds(); /** + * Return true if the vibrator manager has all capabilities, false otherwise. + * @hide + */ + public boolean hasCapabilities(int capabilities) { + return false; + } + + /** * Retrieve a single vibrator by id. * * @param vibratorId The id of the vibrator to be retrieved. @@ -190,4 +201,30 @@ public abstract class VibratorManager { */ @RequiresPermission(android.Manifest.permission.VIBRATE) public abstract void cancel(int usageFilter); + + + /** + * Starts a vibration session on given vibrators. + * + * @param vibratorIds The vibrators that will be controlled by this session. + * @param attrs The {@link VibrationAttributes} corresponding to the vibrations that will + * be performed in the session. This will be used to decide the priority of + * this session against other system vibrations. + * @param reason The description for this session, used for debugging purposes. + * @param cancellationSignal A signal to cancel the session before it starts. + * @param executor The executor for the session callbacks. + * @param callback The {@link VendorVibrationSession.Callback} for the started session. + * @see Vibrator#startVendorSession + * @hide + */ + @RequiresPermission(allOf = { + android.Manifest.permission.VIBRATE, + android.Manifest.permission.VIBRATE_VENDOR_EFFECTS, + android.Manifest.permission.START_VIBRATION_SESSIONS, + }) + public void startVendorSession(@NonNull int[] vibratorIds, @NonNull VibrationAttributes attrs, + @Nullable String reason, @Nullable CancellationSignal cancellationSignal, + @NonNull Executor executor, @NonNull VendorVibrationSession.Callback callback) { + Log.w(TAG, "startVendorSession is not supported"); + } } diff --git a/core/java/android/os/vibrator/IVibrationSession.aidl b/core/java/android/os/vibrator/IVibrationSession.aidl new file mode 100644 index 000000000000..e8295492665d --- /dev/null +++ b/core/java/android/os/vibrator/IVibrationSession.aidl @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.vibrator; + +import android.os.CombinedVibration; + +/** + * The communication channel by which an app control the system vibrators. + * + * In order to synchronize the places where vibrations might be controlled we provide this interface + * so the vibrator subsystem has a chance to: + * + * 1) Decide whether the current session should have the vibrator control. + * 2) Stop any on-going session for a new session/vibration, based on current system policy. + * {@hide} + */ +interface IVibrationSession { + const int STATUS_UNKNOWN = 0; + const int STATUS_SUCCESS = 1; + const int STATUS_IGNORED = 2; + const int STATUS_UNSUPPORTED = 3; + const int STATUS_CANCELED = 4; + const int STATUS_UNKNOWN_ERROR = 5; + + /** + * A method called to start a vibration within this session. This will fail if the session + * is finishing or was canceled. + */ + void vibrate(in CombinedVibration vibration, String reason); + + /** + * A method called by the app to stop this session gracefully. The vibrator will complete any + * ongoing vibration before the session is ended. + */ + void finishSession(); + + /** + * A method called by the app to stop this session immediatelly by interrupting any ongoing + * vibration. + */ + void cancelSession(); +} diff --git a/core/java/android/os/vibrator/IVibrationSessionCallback.aidl b/core/java/android/os/vibrator/IVibrationSessionCallback.aidl new file mode 100644 index 000000000000..36c3695a1bfe --- /dev/null +++ b/core/java/android/os/vibrator/IVibrationSessionCallback.aidl @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2024, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.os.vibrator; + +import android.os.vibrator.IVibrationSession; + +/** + * Callback for vibration session state. + * {@hide} + */ +oneway interface IVibrationSessionCallback { + + /** + * A method called by the service after a vibration session has successfully started. After this + * is called the app has control over the vibrator through this given session. + */ + void onStarted(in IVibrationSession session); + + /** + * A method called by the service to indicate the session is ending and should no longer receive + * vibration requests. + */ + void onFinishing(); + + /** + * A method called by the service after the session has ended. This might be triggered by the + * app or the service. The status code indicates the end reason. + */ + void onFinished(int status); +} diff --git a/core/java/android/os/vibrator/VendorVibrationSession.java b/core/java/android/os/vibrator/VendorVibrationSession.java new file mode 100644 index 000000000000..c23f2ed1a303 --- /dev/null +++ b/core/java/android/os/vibrator/VendorVibrationSession.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.vibrator; + +import static android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.os.CombinedVibration; +import android.os.RemoteException; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * A vendor session that temporarily gains control over the system vibrators. + * + * <p>Vibration effects can be played by the vibrator in a vendor session via {@link #vibrate}. The + * effects will be forwarded to the vibrator hardware immediately. Any concurrency support is + * defined and controlled by the vibrator hardware implementation. + * + * <p>The session should be ended by {@link #close()}, which will wait until the last vibration ends + * and the vibrator is released. The end of the session will be notified to the {@link Callback} + * provided when the session was created. + * + * <p>Any ongoing session can be immediately interrupted by the vendor app via {@link #cancel()}, + * including after {@link #close()} was called and the session is tearing down. A session can also + * be canceled by the vibrator service when it needs to regain control of the system vibrators. + * + * @see Vibrator#startVendorSession + * @hide + */ +@FlaggedApi(FLAG_VENDOR_VIBRATION_EFFECTS) +@SystemApi +public final class VendorVibrationSession implements AutoCloseable { + private static final String TAG = "VendorVibrationSession"; + + /** + * The session ended successfully. + */ + public static final int STATUS_SUCCESS = IVibrationSession.STATUS_SUCCESS; + + /** + * The session was ignored. + * + * <p>This might be caused by user settings, vibration policies or the device state that + * prevents the app from performing vibrations for the requested + * {@link android.os.VibrationAttributes}. + */ + public static final int STATUS_IGNORED = IVibrationSession.STATUS_IGNORED; + + /** + * The session is not supported. + * + * <p>The support for vendor vibration sessions can be checked via + * {@link Vibrator#areVendorSessionsSupported()}. + */ + public static final int STATUS_UNSUPPORTED = IVibrationSession.STATUS_UNSUPPORTED; + + /** + * The session was canceled. + * + * <p>This might be triggered by the app after a session starts via {@link #cancel()}, or it + * can be triggered by the platform before or after the session has started. + */ + public static final int STATUS_CANCELED = IVibrationSession.STATUS_CANCELED; + + /** + * The session status is unknown. + */ + public static final int STATUS_UNKNOWN = IVibrationSession.STATUS_UNKNOWN; + + /** + * The session failed with unknown error. + * + * <p>This can be caused by a failure to start a vibration session or after it has started, to + * indicate it has ended unexpectedly because of a system failure. + */ + public static final int STATUS_UNKNOWN_ERROR = IVibrationSession.STATUS_UNKNOWN_ERROR; + + /** @hide */ + @IntDef(prefix = { "STATUS_" }, value = { + STATUS_SUCCESS, + STATUS_IGNORED, + STATUS_UNSUPPORTED, + STATUS_CANCELED, + STATUS_UNKNOWN, + STATUS_UNKNOWN_ERROR, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Status{} + + private final IVibrationSession mSession; + + /** @hide */ + public VendorVibrationSession(@NonNull IVibrationSession session) { + Objects.requireNonNull(session); + mSession = session; + } + + /** + * Vibrate with a given effect. + * + * <p>The vibration will be sent to the vibrator hardware immediately, without waiting for any + * previous vibration completion. The vendor should control the concurrency behavior at the + * hardware level (e.g. queueing, mixing, interrupting). + * + * <p>If the provided effect is played by the vibrator service with controlled timings (e.g. + * effects created via {@link VibrationEffect#createWaveform}), then triggering a new vibration + * will cause the ongoing playback to be interrupted in favor of the new vibration. If the + * effect is broken down into multiple consecutive commands (e.g. large primitive compositions) + * then the hardware commands will be triggered in succession without waiting for the completion + * callback. + * + * <p>The vendor app is responsible for timing the session requests and the vibrator hardware + * implementation is free to handle concurrency with different policies. + * + * @param effect The {@link VibrationEffect} describing the vibration to be performed. + * @param reason The description for the vibration reason, for debugging purposes. + */ + @RequiresPermission(android.Manifest.permission.VIBRATE) + public void vibrate(@NonNull VibrationEffect effect, @Nullable String reason) { + try { + mSession.vibrate(CombinedVibration.createParallel(effect), reason); + } catch (RemoteException e) { + Log.w(TAG, "Failed to vibrate in a vendor vibration session.", e); + e.rethrowFromSystemServer(); + } + } + + /** + * Cancel ongoing session. + * + * <p>This will stop the vibration immediately and return the vibrator control to the + * platform. This can also be triggered after {@link #close()} to immediately release the + * vibrator. + * + * <p>This will trigger {@link VendorVibrationSession.Callback#onFinished} directly with + * {@link #STATUS_CANCELED}. + */ + public void cancel() { + try { + mSession.cancelSession(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to cancel vendor vibration session.", e); + e.rethrowFromSystemServer(); + } + } + + /** + * End ongoing session gracefully. + * + * <p>This might continue the vibration while it's ramping down and wrapping up the session + * in the vibrator hardware. No more vibration commands can be sent through this session + * after this method is called. + * + * <p>This will trigger {@link VendorVibrationSession.Callback#onFinishing()}. + */ + @Override + public void close() { + try { + mSession.finishSession(); + } catch (RemoteException e) { + Log.w(TAG, "Failed to finish vendor vibration session.", e); + e.rethrowFromSystemServer(); + } + } + + /** + * Callbacks for {@link VendorVibrationSession} events. + * + * @see Vibrator#startVendorSession + * @see VendorVibrationSession + */ + public interface Callback { + + /** + * New session was successfully started. + * + * <p>The vendor app can interact with the vibrator using the + * {@link VendorVibrationSession} provided. + */ + void onStarted(@NonNull VendorVibrationSession session); + + /** + * The session is ending and finishing any pending vibrations. + * + * <p>This is only invoked after {@link #onStarted(VendorVibrationSession)}. It will be + * triggered by both {@link VendorVibrationSession#cancel()} and + * {@link VendorVibrationSession#close()}. This might also be triggered if the platform + * cancels the ongoing session. + * + * <p>Session vibrations might be still ongoing in the vibrator hardware but the app can + * no longer send commands through the session. A finishing session can still be immediately + * stopped via calls to {@link VendorVibrationSession.Callback#cancel()}. + */ + void onFinishing(); + + /** + * The session is finished. + * + * <p>The vibrator has finished any vibration and returned to the platform's control. This + * might be triggered by the vendor app or by the vibrator service. + * + * <p>If this is triggered before {@link #onStarted} then the session was finished before + * starting, either because it was cancelled or failed to start. If the session has already + * started then this will be triggered after {@link #onFinishing()} to indicate all session + * vibrations are complete and the vibrator is no longer under the session's control. + * + * @param status The session status. + */ + void onFinished(@VendorVibrationSession.Status int status); + } +} diff --git a/core/java/android/service/settings/preferences/GetValueRequest.aidl b/core/java/android/service/settings/preferences/GetValueRequest.aidl new file mode 100644 index 000000000000..2a0eb09aa2a4 --- /dev/null +++ b/core/java/android/service/settings/preferences/GetValueRequest.aidl @@ -0,0 +1,4 @@ +package android.service.settings.preferences; + +/** @hide */ +parcelable GetValueRequest;
\ No newline at end of file diff --git a/core/java/android/service/settings/preferences/GetValueRequest.java b/core/java/android/service/settings/preferences/GetValueRequest.java new file mode 100644 index 000000000000..4f82800d1855 --- /dev/null +++ b/core/java/android/service/settings/preferences/GetValueRequest.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.service.settings.preferences; + +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import com.android.settingslib.flags.Flags; + +import java.util.Objects; + +/** + * Request parameters to retrieve the current value of a Settings Preference. + * + * <p>This object passed to {@link SettingsPreferenceService#onGetPreferenceValue} will result + * in a {@link GetValueResult}. + * + * <ul> + * <li>{@link #getScreenKey} is a parameter to distinguish the container screen + * of a preference as a preference key may not be unique within its application. + * <li>{@link #getPreferenceKey} is a parameter to identify the preference for which the value is + * being requested. These keys will be unique with their Preference Screen, but may not be unique + * within their application, so it is required to pair this with {@link #getScreenKey} to + * ensure this request matches the intended target. + * </ul> + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public final class GetValueRequest implements Parcelable { + + @NonNull + private final String mScreenKey; + @NonNull + private final String mPreferenceKey; + + /** + * Returns the screen key of requested Preference. + */ + @NonNull + public String getScreenKey() { + return mScreenKey; + } + + /** + * Returns the key of requested Preference. + */ + @NonNull + public String getPreferenceKey() { + return mPreferenceKey; + } + + private GetValueRequest(@NonNull Builder builder) { + mScreenKey = builder.mScreenKey; + mPreferenceKey = builder.mPreferenceKey; + } + + private GetValueRequest(@NonNull Parcel in) { + mScreenKey = Objects.requireNonNull(in.readString8()); + mPreferenceKey = Objects.requireNonNull(in.readString8()); + } + + /** @hide */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mScreenKey); + dest.writeString8(mPreferenceKey); + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** + * Parcelable Creator for {@link GetValueRequest}. + */ + @NonNull + public static final Creator<GetValueRequest> CREATOR = new Creator<GetValueRequest>() { + @Override + public GetValueRequest createFromParcel(@NonNull Parcel in) { + return new GetValueRequest(in); + } + + @Override + public GetValueRequest[] newArray(int size) { + return new GetValueRequest[size]; + } + }; + + /** + * Builder to construct {@link GetValueRequest}. + */ + public static final class Builder { + private final String mScreenKey; + private final String mPreferenceKey; + + /** + * Create Builder instance. + * @param screenKey required to be not empty + * @param preferenceKey required to be not empty + */ + public Builder(@NonNull String screenKey, @NonNull String preferenceKey) { + if (TextUtils.isEmpty(screenKey)) { + throw new IllegalArgumentException("screenKey cannot be empty"); + } + if (TextUtils.isEmpty(preferenceKey)) { + throw new IllegalArgumentException("preferenceKey cannot be empty"); + } + mScreenKey = screenKey; + mPreferenceKey = preferenceKey; + } + + /** + * Constructs an immutable {@link GetValueRequest} object. + */ + @NonNull + public GetValueRequest build() { + return new GetValueRequest(this); + } + } +} diff --git a/core/java/android/service/settings/preferences/GetValueResult.aidl b/core/java/android/service/settings/preferences/GetValueResult.aidl new file mode 100644 index 000000000000..b5ebd35a3a37 --- /dev/null +++ b/core/java/android/service/settings/preferences/GetValueResult.aidl @@ -0,0 +1,4 @@ +package android.service.settings.preferences; + +/** @hide */ +parcelable GetValueResult;
\ No newline at end of file diff --git a/core/java/android/service/settings/preferences/GetValueResult.java b/core/java/android/service/settings/preferences/GetValueResult.java new file mode 100644 index 000000000000..369dea77cc85 --- /dev/null +++ b/core/java/android/service/settings/preferences/GetValueResult.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.service.settings.preferences; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settingslib.flags.Flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Result object given a corresponding {@link GetValueRequest}. + * <ul> + * <li>If the request was successful, {@link #getResultCode} will be {@link #RESULT_OK}, + * {@link #getValue} will be populated with the settings preference value and + * {@link #getMetadata} will be populated with its metadata. + * <li>If the request is unsuccessful, {@link #getResultCode} be a value other than + * {@link #RESULT_OK} - see documentation for those possibilities to understand the cause + * of the failure. + * </ul> + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public final class GetValueResult implements Parcelable { + + @ResultCode + private final int mResultCode; + @Nullable + private final SettingsPreferenceValue mValue; + @Nullable + private final SettingsPreferenceMetadata mMetadata; + + /** + * Returns the result code indicating status of the request. + */ + @ResultCode + public int getResultCode() { + return mResultCode; + } + + /** + * Returns the value of requested Preference if request successful. + */ + @Nullable + public SettingsPreferenceValue getValue() { + return mValue; + } + + /** + * Returns the metadata of requested Preference if request successful. + */ + @Nullable + public SettingsPreferenceMetadata getMetadata() { + return mMetadata; + } + + /** @hide */ + @IntDef(prefix = { "RESULT_" }, value = { + RESULT_OK, + RESULT_UNSUPPORTED, + RESULT_UNAVAILABLE, + RESULT_REQUIRE_APP_PERMISSION, + RESULT_DISALLOW, + RESULT_INVALID_REQUEST, + RESULT_INTERNAL_ERROR, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResultCode { + } + + /** Request is successful. */ + public static final int RESULT_OK = 0; + /** + * Requested preference is not supported by this API. + * <p>Retry not advised. + */ + public static final int RESULT_UNSUPPORTED = 1; + /** + * Preference is currently not available, likely due to device state or the state of + * a dependency. + * <p>Retry may succeed if underlying conditions change. + */ + public static final int RESULT_UNAVAILABLE = 2; + /** + * Requested preference requires permissions not held by the calling application. + * <p>Retry may succeed if necessary permissions are obtained. + */ + public static final int RESULT_REQUIRE_APP_PERMISSION = 3; + /** + * Requested preference is not allowed for access in this API under the current device policy. + * <p>Retry may succeed if underlying conditions change. + */ + public static final int RESULT_DISALLOW = 4; + /** + * Request object is not valid. + * <p>Retry not advised with current parameters. + */ + public static final int RESULT_INVALID_REQUEST = 5; + /** + * API call failed due to an issue with the service binding. + * <p>Retry may succeed. + */ + public static final int RESULT_INTERNAL_ERROR = 6; + + + private GetValueResult(@NonNull Builder builder) { + mResultCode = builder.mResultCode; + mValue = builder.mValue; + mMetadata = builder.mMetadata; + } + + private GetValueResult(@NonNull Parcel in) { + mResultCode = in.readInt(); + mValue = in.readParcelable(SettingsPreferenceValue.class.getClassLoader(), + SettingsPreferenceValue.class); + mMetadata = in.readParcelable(SettingsPreferenceMetadata.class.getClassLoader(), + SettingsPreferenceMetadata.class); + } + + /** @hide */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mResultCode); + dest.writeParcelable(mValue, flags); + dest.writeParcelable(mMetadata, flags); + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** + * Parcelable Creator for {@link GetValueResult}. + */ + @NonNull + public static final Creator<GetValueResult> CREATOR = new Creator<>() { + @Override + public GetValueResult createFromParcel(@NonNull Parcel in) { + return new GetValueResult(in); + } + + @Override + public GetValueResult[] newArray(int size) { + return new GetValueResult[size]; + } + }; + + /** + * Builder to construct {@link GetValueResult}. + */ + public static final class Builder { + @ResultCode + private final int mResultCode; + private SettingsPreferenceValue mValue; + private SettingsPreferenceMetadata mMetadata; + + /** + * Create Builder instance. + * @param resultCode indicates status of the request + */ + public Builder(@ResultCode int resultCode) { + mResultCode = resultCode; + } + + /** + * Sets the preference value on the result. + */ + @NonNull + public Builder setValue(@Nullable SettingsPreferenceValue value) { + mValue = value; + return this; + } + + /** + * Sets the metadata on the result. + */ + @NonNull + public Builder setMetadata(@Nullable SettingsPreferenceMetadata metadata) { + mMetadata = metadata; + return this; + } + + /** + * Constructs an immutable {@link GetValueResult} object. + */ + @NonNull + public GetValueResult build() { + return new GetValueResult(this); + } + } +} diff --git a/core/java/android/service/settings/preferences/IGetValueCallback.aidl b/core/java/android/service/settings/preferences/IGetValueCallback.aidl new file mode 100644 index 000000000000..bbc7423f453e --- /dev/null +++ b/core/java/android/service/settings/preferences/IGetValueCallback.aidl @@ -0,0 +1,9 @@ +package android.service.settings.preferences; + +import android.service.settings.preferences.GetValueResult; + +/** @hide */ +oneway interface IGetValueCallback { + void onSuccess(in GetValueResult result) = 1; + void onFailure() = 2; +} diff --git a/core/java/android/service/settings/preferences/IMetadataCallback.aidl b/core/java/android/service/settings/preferences/IMetadataCallback.aidl new file mode 100644 index 000000000000..3bd5ebe93660 --- /dev/null +++ b/core/java/android/service/settings/preferences/IMetadataCallback.aidl @@ -0,0 +1,9 @@ +package android.service.settings.preferences; + +import android.service.settings.preferences.MetadataResult; + +/** @hide */ +oneway interface IMetadataCallback { + void onSuccess(in MetadataResult result); + void onFailure(); +} diff --git a/core/java/android/service/settings/preferences/ISetValueCallback.aidl b/core/java/android/service/settings/preferences/ISetValueCallback.aidl new file mode 100644 index 000000000000..0765660c83c3 --- /dev/null +++ b/core/java/android/service/settings/preferences/ISetValueCallback.aidl @@ -0,0 +1,9 @@ +package android.service.settings.preferences; + +import android.service.settings.preferences.SetValueResult; + +/** @hide */ +oneway interface ISetValueCallback { + void onSuccess(in SetValueResult result); + void onFailure(); +} diff --git a/core/java/android/service/settings/preferences/MetadataRequest.aidl b/core/java/android/service/settings/preferences/MetadataRequest.aidl new file mode 100644 index 000000000000..dc3cbc42661e --- /dev/null +++ b/core/java/android/service/settings/preferences/MetadataRequest.aidl @@ -0,0 +1,4 @@ +package android.service.settings.preferences; + +/** @hide */ +parcelable MetadataRequest;
\ No newline at end of file diff --git a/core/java/android/service/settings/preferences/MetadataRequest.java b/core/java/android/service/settings/preferences/MetadataRequest.java new file mode 100644 index 000000000000..ffecc6bec5b2 --- /dev/null +++ b/core/java/android/service/settings/preferences/MetadataRequest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.settings.preferences; + +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.settingslib.flags.Flags; + +/** + * Request parameters to retrieve all metadata for all available settings preferences within this + * application. + * + * <p>This object passed to {@link SettingsPreferenceService#onGetAllPreferenceMetadata} will result + * in a {@link MetadataResult}. + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public final class MetadataRequest implements Parcelable { + private MetadataRequest() {} + + /** @hide */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** + * Parcelable Creator for {@link MetadataRequest}. + */ + @NonNull + public static final Creator<MetadataRequest> CREATOR = new Creator<>() { + @Override + public MetadataRequest createFromParcel(@NonNull Parcel in) { + return new MetadataRequest(); + } + + @Override + public MetadataRequest[] newArray(int size) { + return new MetadataRequest[size]; + } + }; + + /** + * Builder to construct {@link MetadataRequest}. + */ + public static final class Builder { + /** Constructs an immutable {@link MetadataRequest} object. */ + @NonNull + public MetadataRequest build() { + return new MetadataRequest(); + } + } +} diff --git a/core/java/android/service/settings/preferences/MetadataResult.aidl b/core/java/android/service/settings/preferences/MetadataResult.aidl new file mode 100644 index 000000000000..af9e8a86e3ab --- /dev/null +++ b/core/java/android/service/settings/preferences/MetadataResult.aidl @@ -0,0 +1,4 @@ +package android.service.settings.preferences; + +/** @hide */ +parcelable MetadataResult;
\ No newline at end of file diff --git a/core/java/android/service/settings/preferences/MetadataResult.java b/core/java/android/service/settings/preferences/MetadataResult.java new file mode 100644 index 000000000000..6a65dcc9c757 --- /dev/null +++ b/core/java/android/service/settings/preferences/MetadataResult.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.service.settings.preferences; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.settingslib.flags.Flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Result object given a corresponding {@link MetadataRequest}. + * <ul> + * <li>If the request was successful, {@link #getResultCode} will be {@link #RESULT_OK} and + * {@link #getMetadataList} will be populated with metadata for all available preferences within + * this application. + * <li>If the request is unsuccessful, {@link #getResultCode} be a value other than + * {@link #RESULT_OK} - see documentation for those possibilities to understand the cause + * of the failure. + * </ul> + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public final class MetadataResult implements Parcelable { + + @ResultCode + private final int mResultCode; + @NonNull + private final List<SettingsPreferenceMetadata> mMetadataList; + + /** + * Returns the result code indicating status of the request. + */ + @ResultCode + public int getResultCode() { + return mResultCode; + } + + /** + * Returns the list of available Preference Metadata. + * <p>This instance is shared so this list should not be modified. + */ + @NonNull + public List<SettingsPreferenceMetadata> getMetadataList() { + return mMetadataList; + } + + /** @hide */ + @IntDef(prefix = { "RESULT_" }, value = { + RESULT_OK, + RESULT_UNSUPPORTED, + RESULT_INTERNAL_ERROR + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResultCode { + } + + /** Request is successful. */ + public static final int RESULT_OK = 0; + /** + * No preferences in this application support this API. + * <p>Retry not advised. + */ + public static final int RESULT_UNSUPPORTED = 1; + /** + * API call failed due to an issue with the service binding. + * <p>Retry may succeed. + */ + public static final int RESULT_INTERNAL_ERROR = 2; + + private MetadataResult(@NonNull Builder builder) { + mResultCode = builder.mResultCode; + mMetadataList = builder.mMetadataList; + } + private MetadataResult(@NonNull Parcel in) { + mResultCode = in.readInt(); + mMetadataList = new ArrayList<>(); + in.readTypedList(mMetadataList, SettingsPreferenceMetadata.CREATOR); + } + + /** @hide */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mResultCode); + dest.writeTypedList(mMetadataList, flags); + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** + * Parcelable Creator for {@link MetadataResult}. + */ + @NonNull + public static final Creator<MetadataResult> CREATOR = new Creator<>() { + @Override + public MetadataResult createFromParcel(@NonNull Parcel in) { + return new MetadataResult(in); + } + + @Override + public MetadataResult[] newArray(int size) { + return new MetadataResult[size]; + } + }; + + /** + * Builder to construct {@link MetadataResult}. + */ + public static final class Builder { + @ResultCode + private final int mResultCode; + private List<SettingsPreferenceMetadata> mMetadataList = Collections.emptyList(); + + /** + * Create Builder instance. + * @param resultCode indicates status of the request + */ + public Builder(@ResultCode int resultCode) { + mResultCode = resultCode; + } + + /** + * Sets the metadata list on the result. + */ + @NonNull + public Builder setMetadataList(@NonNull List<SettingsPreferenceMetadata> metadataList) { + mMetadataList = metadataList; + return this; + } + + /** + * Constructs an immutable {@link MetadataResult} object. + */ + @NonNull + public MetadataResult build() { + return new MetadataResult(this); + } + } +} diff --git a/core/java/android/service/settings/preferences/SetValueRequest.aidl b/core/java/android/service/settings/preferences/SetValueRequest.aidl new file mode 100644 index 000000000000..198e333d5cb6 --- /dev/null +++ b/core/java/android/service/settings/preferences/SetValueRequest.aidl @@ -0,0 +1,4 @@ +package android.service.settings.preferences; + +/** @hide */ +parcelable SetValueRequest;
\ No newline at end of file diff --git a/core/java/android/service/settings/preferences/SetValueRequest.java b/core/java/android/service/settings/preferences/SetValueRequest.java new file mode 100644 index 000000000000..f7600aecdfaf --- /dev/null +++ b/core/java/android/service/settings/preferences/SetValueRequest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.service.settings.preferences; + +import android.annotation.FlaggedApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import com.android.settingslib.flags.Flags; + +import java.util.Objects; + +/** + * Request parameters to set the current value to a Settings Preference. + * <p>This object passed to {@link SettingsPreferenceService#onSetPreferenceValue} will result in a + * {@link SetValueResult}. + * <ul> + * <li>{@link #getScreenKey} is a parameter to distinguish the container screen + * of a preference as a preference key may not be unique within its application. + * <li>{@link #getPreferenceKey} is a parameter to identify the preference for which the value is + * being requested. These keys will be unique with their Preference Screen, but may not be unique + * within their application, so it is required to pair this with {@link #getScreenKey} to + * ensure this request matches the intended target. + * <li>{@link #getPreferenceValue} is a parameter to specify the value that this request aims to + * set. If this value is invalid (malformed or does not match the type of the preference) then + * this request will fail. + * </ul> + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public final class SetValueRequest implements Parcelable { + + @NonNull + private final String mScreenKey; + @NonNull + private final String mPreferenceKey; + @NonNull + private final SettingsPreferenceValue mPreferenceValue; + + /** + * Returns the screen key of requested Preference. + */ + @NonNull + public String getScreenKey() { + return mScreenKey; + } + + /** + * Returns the key of requested Preference. + */ + @NonNull + public String getPreferenceKey() { + return mPreferenceKey; + } + + /** + * Returns the value of requested Preference. + */ + @NonNull + public SettingsPreferenceValue getPreferenceValue() { + return mPreferenceValue; + } + + private SetValueRequest(@NonNull Builder builder) { + mScreenKey = builder.mScreenKey; + mPreferenceKey = builder.mPreferenceKey; + mPreferenceValue = builder.mPreferenceValue; + } + + private SetValueRequest(@NonNull Parcel in) { + mScreenKey = Objects.requireNonNull(in.readString8()); + mPreferenceKey = Objects.requireNonNull(in.readString8()); + mPreferenceValue = Objects.requireNonNull(in.readParcelable( + SettingsPreferenceValue.class.getClassLoader(), SettingsPreferenceValue.class)); + } + + /** @hide */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mScreenKey); + dest.writeString8(mPreferenceKey); + dest.writeParcelable(mPreferenceValue, flags); + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** + * Parcelable Creator for {@link SetValueRequest}. + */ + @NonNull + public static final Creator<SetValueRequest> CREATOR = new Creator<SetValueRequest>() { + @Override + public SetValueRequest createFromParcel(@NonNull Parcel in) { + return new SetValueRequest(in); + } + + @Override + public SetValueRequest[] newArray(int size) { + return new SetValueRequest[size]; + } + }; + + /** + * Builder to construct {@link SetValueRequest}. + */ + public static final class Builder { + private final String mScreenKey; + private final String mPreferenceKey; + private final SettingsPreferenceValue mPreferenceValue; + + /** + * Create Builder instance. + * @param screenKey required to be not empty + * @param preferenceKey required to be not empty + * @param value value to set to requested Preference + */ + public Builder(@NonNull String screenKey, @NonNull String preferenceKey, + @NonNull SettingsPreferenceValue value) { + if (TextUtils.isEmpty(screenKey)) { + throw new IllegalArgumentException("screenKey cannot be empty"); + } + if (TextUtils.isEmpty(preferenceKey)) { + throw new IllegalArgumentException("preferenceKey cannot be empty"); + } + mScreenKey = screenKey; + mPreferenceKey = preferenceKey; + mPreferenceValue = value; + } + + /** + * Constructs an immutable {@link SetValueRequest} object. + */ + @NonNull + public SetValueRequest build() { + return new SetValueRequest(this); + } + } +} diff --git a/core/java/android/service/settings/preferences/SetValueResult.aidl b/core/java/android/service/settings/preferences/SetValueResult.aidl new file mode 100644 index 000000000000..f54813484d68 --- /dev/null +++ b/core/java/android/service/settings/preferences/SetValueResult.aidl @@ -0,0 +1,4 @@ +package android.service.settings.preferences; + +/** @hide */ +parcelable SetValueResult;
\ No newline at end of file diff --git a/core/java/android/service/settings/preferences/SetValueResult.java b/core/java/android/service/settings/preferences/SetValueResult.java new file mode 100644 index 000000000000..cb1776abd3bc --- /dev/null +++ b/core/java/android/service/settings/preferences/SetValueResult.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.service.settings.preferences; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.android.settingslib.flags.Flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Result object given a corresponding {@link SetValueRequest}. + * <ul> + * <li>If the request was successful, {@link #getResultCode} will be {@link #RESULT_OK}. + * <li>If the request is unsuccessful, {@link #getResultCode} be a value other than + * {@link #RESULT_OK} - see documentation for those possibilities to understand the cause + * of the failure. + * </ul> + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public final class SetValueResult implements Parcelable { + + @ResultCode + private final int mResultCode; + + /** + * Returns the result code indicating status of the request. + */ + @ResultCode + public int getResultCode() { + return mResultCode; + } + + /** @hide */ + @IntDef(prefix = { "RESULT_" }, value = { + RESULT_OK, + RESULT_UNSUPPORTED, + RESULT_DISABLED, + RESULT_RESTRICTED, + RESULT_UNAVAILABLE, + RESULT_REQUIRE_APP_PERMISSION, + RESULT_REQUIRE_USER_CONSENT, + RESULT_DISALLOW, + RESULT_INVALID_REQUEST, + RESULT_INTERNAL_ERROR + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ResultCode { + } + + /** Request is successful and the value was set. */ + public static final int RESULT_OK = 0; + /** + * Requested preference is not supported by this API. + * <p>Retry not advised. + */ + public static final int RESULT_UNSUPPORTED = 1; + /** + * Requested preference is disabled, thus unable to be set in this state. + * <p>Retry may succeed if underlying conditions change. + */ + public static final int RESULT_DISABLED = 2; + /** + * Requested preference is restricted, thus unable to be set under this policy. + * <p>Retry may succeed if underlying conditions change. + */ + public static final int RESULT_RESTRICTED = 3; + /** + * Preference is currently not available, likely due to device state or the state of + * a dependency. + * <p>Retry may succeed if underlying conditions change. + */ + public static final int RESULT_UNAVAILABLE = 4; + /** + * Requested preference requires permissions not held by the calling application. + * <p>Retry may succeed if necessary permissions are obtained. + */ + public static final int RESULT_REQUIRE_APP_PERMISSION = 5; + /** + * User consent was not approved for this operation. + * <p>Retry may succeed if user provides consent. + */ + public static final int RESULT_REQUIRE_USER_CONSENT = 6; + /** + * Requested preference is not allowed for access in this API under the current device policy. + * <p>Retry may succeed if underlying conditions change. + */ + public static final int RESULT_DISALLOW = 7; + /** + * Request object is not valid. + * <p>Retry not advised with current parameters. + */ + public static final int RESULT_INVALID_REQUEST = 8; + /** + * API call failed due to an issue with the service binding. + * <p>Retry may succeed. + */ + public static final int RESULT_INTERNAL_ERROR = 9; + + private SetValueResult(@NonNull Builder builder) { + mResultCode = builder.mResultCode; + } + + private SetValueResult(@NonNull Parcel in) { + mResultCode = in.readInt(); + } + + /** @hide */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mResultCode); + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** + * Parcelable Creator for {@link SetValueResult}. + */ + @NonNull + public static final Creator<SetValueResult> CREATOR = new Creator<>() { + @Override + public SetValueResult createFromParcel(@NonNull Parcel in) { + return new SetValueResult(in); + } + + @Override + public SetValueResult[] newArray(int size) { + return new SetValueResult[size]; + } + }; + + /** + * Builder to construct {@link SetValueResult}. + */ + public static final class Builder { + @ResultCode + private final int mResultCode; + + /** + * Create Builder instance. + * @param resultCode indicates status of the request + */ + public Builder(@ResultCode int resultCode) { + mResultCode = resultCode; + } + + /** + * Constructs an immutable {@link SetValueResult} object. + */ + @NonNull + public SetValueResult build() { + return new SetValueResult(this); + } + } +} diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceMetadata.java b/core/java/android/service/settings/preferences/SettingsPreferenceMetadata.java new file mode 100644 index 000000000000..1d08c5217129 --- /dev/null +++ b/core/java/android/service/settings/preferences/SettingsPreferenceMetadata.java @@ -0,0 +1,436 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.service.settings.preferences; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settingslib.flags.Flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Data object representation of a Settings Preference definition and state. + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public final class SettingsPreferenceMetadata implements Parcelable { + + @NonNull + private final String mKey; + @NonNull + private final String mScreenKey; + @Nullable + private final String mTitle; + @Nullable + private final String mSummary; + @NonNull + private final List<String> mBreadcrumbs; + @NonNull + private final List<String> mReadPermissions; + @NonNull + private final List<String> mWritePermissions; + private final boolean mEnabled; + private final boolean mAvailable; + private final boolean mWritable; + private final boolean mRestricted; + private final int mSensitivity; + @Nullable + private final PendingIntent mLaunchIntent; + @NonNull + private final Bundle mExtras; + + /** + * Returns the key of Preference. + */ + @NonNull + public String getKey() { + return mKey; + } + + /** + * Returns the screen key of Preference. + */ + @NonNull + public String getScreenKey() { + return mScreenKey; + } + + /** + * Returns the title of Preference. + */ + @Nullable + public String getTitle() { + return mTitle; + } + + /** + * Returns the summary of Preference. + */ + @Nullable + public String getSummary() { + return mSummary; + } + + /** + * Returns the breadcrumbs (navigation context) of Preference. + * <p>May be empty. + */ + @NonNull + public List<String> getBreadcrumbs() { + return mBreadcrumbs; + } + + /** + * Returns the permissions required to read this Preference's value. + * <p>May be empty. + */ + @NonNull + public List<String> getReadPermissions() { + return mReadPermissions; + } + + /** + * Returns the permissions required to write this Preference's value. + * <p>May be empty. + */ + @NonNull + public List<String> getWritePermissions() { + return mWritePermissions; + } + + /** + * Returns whether Preference is enabled. + */ + public boolean isEnabled() { + return mEnabled; + } + + /** + * Returns whether Preference is available. + */ + public boolean isAvailable() { + return mAvailable; + } + + /** + * Returns whether Preference is writable. + */ + public boolean isWritable() { + return mWritable; + } + + /** + * Returns whether Preference is restricted. + */ + public boolean isRestricted() { + return mRestricted; + } + + /** + * Returns the write-level sensitivity of Preference. + */ + @WriteSensitivity + public int getWriteSensitivity() { + return mSensitivity; + } + + /** + * Returns the intent to launch the host app page for this Preference. + */ + @Nullable + public PendingIntent getLaunchIntent() { + return mLaunchIntent; + } + + /** + * Returns any additional fields specific to this preference. + * <p>Treat all data as optional. + */ + @NonNull + public Bundle getExtras() { + return mExtras; + } + + /** @hide */ + @IntDef(value = { + NOT_SENSITIVE, + SENSITIVE, + INTENT_ONLY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface WriteSensitivity {} + + /** + * Preference is not sensitive, thus its value is writable without explicit consent, assuming + * all necessary permissions are granted. + */ + public static final int NOT_SENSITIVE = 0; + /** + * Preference is sensitive, meaning that in addition to necessary permissions, writing its value + * will also request explicit user consent. + */ + public static final int SENSITIVE = 1; + /** + * Preference is not permitted for write-access via API and must be changed via Settings page. + */ + public static final int INTENT_ONLY = 2; + + private SettingsPreferenceMetadata(@NonNull Builder builder) { + mKey = builder.mKey; + mScreenKey = builder.mScreenKey; + mTitle = builder.mTitle; + mSummary = builder.mSummary; + mBreadcrumbs = builder.mBreadcrumbs; + mReadPermissions = builder.mReadPermissions; + mWritePermissions = builder.mWritePermissions; + mEnabled = builder.mEnabled; + mAvailable = builder.mAvailable; + mWritable = builder.mWritable; + mRestricted = builder.mRestricted; + mSensitivity = builder.mSensitivity; + mLaunchIntent = builder.mLaunchIntent; + mExtras = Objects.requireNonNullElseGet(builder.mExtras, Bundle::new); + } + @SuppressLint("ParcelClassLoader") + private SettingsPreferenceMetadata(@NonNull Parcel in) { + mKey = Objects.requireNonNull(in.readString8()); + mScreenKey = Objects.requireNonNull(in.readString8()); + mTitle = in.readString8(); + mSummary = in.readString8(); + mBreadcrumbs = new ArrayList<>(); + in.readStringList(mBreadcrumbs); + mReadPermissions = new ArrayList<>(); + in.readStringList(mReadPermissions); + mWritePermissions = new ArrayList<>(); + in.readStringList(mWritePermissions); + mEnabled = in.readBoolean(); + mAvailable = in.readBoolean(); + mWritable = in.readBoolean(); + mRestricted = in.readBoolean(); + mSensitivity = in.readInt(); + mLaunchIntent = in.readParcelable(PendingIntent.class.getClassLoader(), + PendingIntent.class); + mExtras = Objects.requireNonNullElseGet(in.readBundle(), Bundle::new); + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mKey); + dest.writeString8(mScreenKey); + dest.writeString8(mTitle); + dest.writeString8(mSummary); + dest.writeStringList(mBreadcrumbs); + dest.writeStringList(mReadPermissions); + dest.writeStringList(mWritePermissions); + dest.writeBoolean(mEnabled); + dest.writeBoolean(mAvailable); + dest.writeBoolean(mWritable); + dest.writeBoolean(mRestricted); + dest.writeInt(mSensitivity); + dest.writeParcelable(mLaunchIntent, flags); + dest.writeBundle(mExtras); + } + + /** + * Parcelable Creator for {@link SettingsPreferenceMetadata}. + */ + @NonNull + public static final Creator<SettingsPreferenceMetadata> CREATOR = new Creator<>() { + @Override + public SettingsPreferenceMetadata createFromParcel(@NonNull Parcel in) { + return new SettingsPreferenceMetadata(in); + } + + @Override + public SettingsPreferenceMetadata[] newArray(int size) { + return new SettingsPreferenceMetadata[size]; + } + }; + + /** + * Builder to construct {@link SettingsPreferenceMetadata}. + */ + public static final class Builder { + private final String mScreenKey; + private final String mKey; + private String mTitle; + private String mSummary; + private List<String> mBreadcrumbs = Collections.emptyList(); + private List<String> mReadPermissions = Collections.emptyList(); + private List<String> mWritePermissions = Collections.emptyList(); + private boolean mEnabled = false; + private boolean mAvailable = false; + private boolean mWritable = false; + private boolean mRestricted = false; + @WriteSensitivity private int mSensitivity = INTENT_ONLY; + private PendingIntent mLaunchIntent; + private Bundle mExtras; + + /** + * Create Builder instance. + * @param screenKey required to be not empty + * @param key required to be not empty + */ + public Builder(@NonNull String screenKey, @NonNull String key) { + if (TextUtils.isEmpty(screenKey)) { + throw new IllegalArgumentException("screenKey cannot be empty"); + } + if (TextUtils.isEmpty(key)) { + throw new IllegalArgumentException("key cannot be empty"); + } + mScreenKey = screenKey; + mKey = key; + } + + /** + * Sets the preference title. + */ + @NonNull + public Builder setTitle(@Nullable String title) { + mTitle = title; + return this; + } + + /** + * Sets the preference summary. + */ + @NonNull + public Builder setSummary(@Nullable String summary) { + mSummary = summary; + return this; + } + + /** + * Sets the preference breadcrumbs (navigation context). + */ + @NonNull + public Builder setBreadcrumbs(@NonNull List<String> breadcrumbs) { + mBreadcrumbs = breadcrumbs; + return this; + } + + /** + * Sets the permissions required for reading this preference. + */ + @NonNull + public Builder setReadPermissions(@NonNull List<String> readPermissions) { + mReadPermissions = readPermissions; + return this; + } + + /** + * Sets the permissions required for writing this preference. + */ + @NonNull + public Builder setWritePermissions(@NonNull List<String> writePermissions) { + mWritePermissions = writePermissions; + return this; + } + + /** + * Set whether the preference is enabled. + */ + @NonNull + public Builder setEnabled(boolean enabled) { + mEnabled = enabled; + return this; + } + + /** + * Sets whether the preference is available. + */ + @NonNull + public Builder setAvailable(boolean available) { + mAvailable = available; + return this; + } + + /** + * Sets whether the preference is writable. + */ + @NonNull + public Builder setWritable(boolean writable) { + mWritable = writable; + return this; + } + + /** + * Sets whether the preference is restricted. + */ + @NonNull + public Builder setRestricted(boolean restricted) { + mRestricted = restricted; + return this; + } + + /** + * Sets the preference write-level sensitivity. + */ + @NonNull + public Builder setWriteSensitivity(@WriteSensitivity int sensitivity) { + mSensitivity = sensitivity; + return this; + } + + /** + * Sets the intent to launch the host app page for this preference. + */ + @NonNull + public Builder setLaunchIntent(@Nullable PendingIntent launchIntent) { + mLaunchIntent = launchIntent; + return this; + } + + /** + * Sets additional fields specific to this preference. Treat all data as optional. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Constructs an immutable {@link SettingsPreferenceMetadata} object. + */ + @NonNull + public SettingsPreferenceMetadata build() { + return new SettingsPreferenceMetadata(this); + } + } +} diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceValue.java b/core/java/android/service/settings/preferences/SettingsPreferenceValue.java new file mode 100644 index 000000000000..f056e34a0dd2 --- /dev/null +++ b/core/java/android/service/settings/preferences/SettingsPreferenceValue.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.service.settings.preferences; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.SuppressLint; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settingslib.flags.Flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This objects represents a value that can be used for a particular settings preference. + * <p>The data type for the value will correspond to {@link #getType}. For possible types, see + * constants below, such as {@link #TYPE_BOOLEAN} and {@link #TYPE_STRING}. + * Depending on the type, the corresponding getter will contain its value. All other getters will + * return default values (boolean returns false, String returns null) so they should not be used. + * <p>See documentation on the constants for which getter method should be used. + */ +@FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) +public final class SettingsPreferenceValue implements Parcelable { + + @Type + private final int mType; + private final boolean mBooleanValue; + private final long mLongValue; + private final double mDoubleValue; + @Nullable + private final String mStringValue; + + /** + * Returns the type indicator for Preference value. + */ + @Type + public int getType() { + return mType; + } + + /** + * Returns the boolean value for Preference if type is {@link #TYPE_BOOLEAN}. + */ + public boolean getBooleanValue() { + return mBooleanValue; + } + + /** + * Returns the long value for Preference if type is {@link #TYPE_LONG}. + */ + public long getLongValue() { + return mLongValue; + } + + /** + * Returns the double value for Preference if type is {@link #TYPE_DOUBLE}. + */ + public double getDoubleValue() { + return mDoubleValue; + } + + /** + * Returns the string value for Preference if type is {@link #TYPE_STRING}. + */ + @Nullable + public String getStringValue() { + return mStringValue; + } + + /** @hide */ + @IntDef(prefix = { "TYPE_" }, value = { + TYPE_BOOLEAN, + TYPE_LONG, + TYPE_DOUBLE, + TYPE_STRING, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Type {} + + /** Value is of type boolean. Access via {@link #getBooleanValue}. */ + public static final int TYPE_BOOLEAN = 0; + /** Value is of type long. Access via {@link #getLongValue()}. */ + public static final int TYPE_LONG = 1; + /** Value is of type double. Access via {@link #getDoubleValue()}. */ + public static final int TYPE_DOUBLE = 2; + /** Value is of type string. Access via {@link #getStringValue}. */ + public static final int TYPE_STRING = 3; + + private SettingsPreferenceValue(@NonNull Builder builder) { + mType = builder.mType; + mBooleanValue = builder.mBooleanValue; + mLongValue = builder.mLongValue; + mDoubleValue = builder.mDoubleValue; + mStringValue = builder.mStringValue; + } + + private SettingsPreferenceValue(@NonNull Parcel in) { + mType = in.readInt(); + mBooleanValue = in.readBoolean(); + mLongValue = in.readLong(); + mDoubleValue = in.readDouble(); + mStringValue = in.readString8(); + } + + /** @hide */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mType); + dest.writeBoolean(mBooleanValue); + dest.writeLong(mLongValue); + dest.writeDouble(mDoubleValue); + dest.writeString8(mStringValue); + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** + * Parcelable Creator for {@link SettingsPreferenceValue}. + */ + @NonNull + public static final Creator<SettingsPreferenceValue> CREATOR = new Creator<>() { + @Override + public SettingsPreferenceValue createFromParcel(@NonNull Parcel in) { + return new SettingsPreferenceValue(in); + } + + @Override + public SettingsPreferenceValue[] newArray(int size) { + return new SettingsPreferenceValue[size]; + } + }; + + /** + * Builder to construct {@link SettingsPreferenceValue}. + */ + public static final class Builder { + @Type + private final int mType; + private boolean mBooleanValue; + private long mLongValue; + private double mDoubleValue; + private String mStringValue; + + /** + * Create Builder instance. + * @param type type indicator for preference value + */ + public Builder(@Type int type) { + mType = type; + } + + /** + * Sets boolean value for Preference. + */ + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public Builder setBooleanValue(boolean booleanValue) { + mBooleanValue = booleanValue; + return this; + } + + /** + * Sets long value for Preference. + */ + @NonNull + public Builder setLongValue(long longValue) { + mLongValue = longValue; + return this; + } + + /** + * Sets floating point value for Preference. + */ + @NonNull + public Builder setDoubleValue(double doubleValue) { + mDoubleValue = doubleValue; + return this; + } + + /** + * Sets string value for Preference. + */ + @NonNull + public Builder setStringValue(@Nullable String stringValue) { + mStringValue = stringValue; + return this; + } + + /** + * Constructs an immutable {@link SettingsPreferenceValue} object. + */ + @NonNull + public SettingsPreferenceValue build() { + return new SettingsPreferenceValue(this); + } + } +} diff --git a/core/java/android/service/wallpaper/IWallpaperService.aidl b/core/java/android/service/wallpaper/IWallpaperService.aidl index f76e6cee13f0..bcdd4775c164 100644 --- a/core/java/android/service/wallpaper/IWallpaperService.aidl +++ b/core/java/android/service/wallpaper/IWallpaperService.aidl @@ -28,6 +28,6 @@ oneway interface IWallpaperService { void attach(IWallpaperConnection connection, IBinder windowToken, int windowType, boolean isPreview, int reqWidth, int reqHeight, in Rect padding, int displayId, int which, - in WallpaperInfo info, in @nullable WallpaperDescription description); + in WallpaperInfo info, in WallpaperDescription description); void detach(IBinder windowToken); } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 131fdc895841..2061abac248e 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -17,6 +17,7 @@ package android.service.wallpaper; import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING; +import static android.app.Flags.liveWallpaperContentHandling; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; import static android.app.WallpaperManager.SetWallpaperFlags; @@ -2624,7 +2625,7 @@ public abstract class WallpaperService extends Service { private void doAttachEngine() { Trace.beginSection("WPMS.onCreateEngine"); Engine engine; - if (mDescription != null) { + if (liveWallpaperContentHandling()) { engine = onCreateEngine(mDescription); } else { engine = onCreateEngine(); diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 8f112f338a00..4ff04d5c1fa6 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -447,7 +447,6 @@ public final class DisplayInfo implements Parcelable { && Objects.equals(displayCutout, other.displayCutout) && rotation == other.rotation && modeId == other.modeId - && renderFrameRate == other.renderFrameRate && hasArrSupport == other.hasArrSupport && Objects.equals(frameRateCategoryRate, other.frameRateCategoryRate) && defaultModeId == other.defaultModeId @@ -705,6 +704,9 @@ public final class DisplayInfo implements Parcelable { if (refreshRateOverride > 0) { return refreshRateOverride; } + if (renderFrameRate > 0) { + return renderFrameRate; + } if (supportedModes.length == 0) { return 0; } diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 9845bf00d937..68e78fed29c5 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -76,6 +76,14 @@ flag { } flag { + name: "disable_opt_out_edge_to_edge" + namespace: "windowing_frontend" + description: "Deprecate and disable windowOptOutEdgeToEdgeEnforcement" + bug: "377864165" + is_fixed_read_only: true +} + +flag { name: "keyguard_going_away_timeout" namespace: "windowing_frontend" description: "Allow a maximum of 10 seconds with keyguardGoingAway=true before force-resetting" diff --git a/core/java/com/android/internal/statusbar/StatusBarIcon.java b/core/java/com/android/internal/statusbar/StatusBarIcon.java index 1938cdb0ba84..40161023eae4 100644 --- a/core/java/com/android/internal/statusbar/StatusBarIcon.java +++ b/core/java/com/android/internal/statusbar/StatusBarIcon.java @@ -40,9 +40,6 @@ public class StatusBarIcon implements Parcelable { public enum Type { // Notification: the sender avatar for important conversations PeopleAvatar, - // Notification: the monochrome version of the app icon if available; otherwise fall back to - // the small icon - MaybeMonochromeAppIcon, // Notification: the small icon from the notification NotifSmallIcon, // The wi-fi, cellular or battery icon. diff --git a/core/java/com/android/internal/widget/NotificationRowIconView.java b/core/java/com/android/internal/widget/NotificationRowIconView.java index adcc0f64b598..5fc61b00e331 100644 --- a/core/java/com/android/internal/widget/NotificationRowIconView.java +++ b/core/java/com/android/internal/widget/NotificationRowIconView.java @@ -22,11 +22,7 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; import android.graphics.Paint; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; @@ -35,8 +31,6 @@ import android.util.AttributeSet; import android.view.RemotableViewMethod; import android.widget.RemoteViews; -import com.android.internal.R; - /** * An image view that holds the icon displayed at the start of a notification row. * This can generally either display the "small icon" of a notification set via @@ -48,7 +42,6 @@ public class NotificationRowIconView extends CachingIconView { private NotificationIconProvider mIconProvider; private boolean mApplyCircularCrop = false; - private boolean mShouldShowAppIcon = false; private Drawable mAppIcon = null; // Padding, background and colors set on the view prior to being overridden when showing the app @@ -77,17 +70,6 @@ public class NotificationRowIconView extends CachingIconView { super(context, attrs, defStyleAttr, defStyleRes); } - @Override - protected void onFinishInflate() { - // If showing the app icon, we don't need background or padding. - if (Flags.notificationsUseAppIcon()) { - setPadding(0, 0, 0, 0); - setBackground(null); - } - - super.onFinishInflate(); - } - /** * Sets the icon provider for this view. This is used to determine whether we should show the * app icon instead of the small icon, and to fetch the app icon if needed. @@ -153,37 +135,12 @@ public class NotificationRowIconView extends CachingIconView { return super.setImageIconAsync(icon); } - /** Whether the icon represents the app icon (instead of the small icon). */ - @RemotableViewMethod - public void setShouldShowAppIcon(boolean shouldShowAppIcon) { - if (Flags.notificationsUseAppIconInRow()) { - if (mShouldShowAppIcon == shouldShowAppIcon) { - return; // no change - } - - mShouldShowAppIcon = shouldShowAppIcon; - if (mShouldShowAppIcon) { - adjustViewForAppIcon(); - } else { - // Restore original padding and background if needed - restoreViewForSmallIcon(); - } - } - } - /** * Override padding and background from the view to display the app icon. */ private void adjustViewForAppIcon() { removePadding(); - - if (Flags.notificationsUseAppIconInRow()) { - addWhiteBackground(); - } else { - // No need to set the background for notification redesign, since the icon - // factory already does that for us. - removeBackground(); - } + removeBackground(); } /** @@ -221,21 +178,6 @@ public class NotificationRowIconView extends CachingIconView { setBackground(null); } - private void addWhiteBackground() { - if (mOriginalBackground == null) { - mOriginalBackground = getBackground(); - } - - // Make the background white in case the icon itself doesn't have one. - ColorFilter colorFilter = new PorterDuffColorFilter(Color.WHITE, - PorterDuff.Mode.SRC_ATOP); - - if (mOriginalBackground == null) { - setBackground(getContext().getDrawable(R.drawable.notification_icon_circle)); - } - getBackground().mutate().setColorFilter(colorFilter); - } - private void restoreBackground() { // NOTE: This will not work if the original background was null, but that's better than // accidentally clearing the background. We expect that there's generally going to be one diff --git a/core/res/Android.bp b/core/res/Android.bp index 66c2e12f7cdf..73776f0d0b1d 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -158,6 +158,7 @@ android_app { flags_packages: [ "android.app.appfunctions.flags-aconfig", "android.app.contextualsearch.flags-aconfig", + "android.app.flags-aconfig", "android.appwidget.flags-aconfig", "android.content.pm.flags-aconfig", "android.provider.flags-aconfig", @@ -172,6 +173,7 @@ android_app { "com.android.hardware.input.input-aconfig", "aconfig_trade_in_mode_flags", "ranging_aconfig_flags", + "aconfig_settingslib_flags", ], } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5913992004b8..0e4eb94feffb 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2632,13 +2632,22 @@ <!-- @SystemApi Allows access to perform vendor effects in the vibrator. <p>Protection level: signature - @FlaggedApi("android.os.vibrator.vendor_vibration_effects") + @FlaggedApi(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) @hide --> <permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" android:protectionLevel="signature|privileged" android:featureFlag="android.os.vibrator.vendor_vibration_effects" /> + <!-- @SystemApi Allows access to start a vendor vibration session. + <p>Protection level: signature + @FlaggedApi(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @hide + --> + <permission android:name="android.permission.START_VIBRATION_SESSIONS" + android:protectionLevel="signature|privileged" + android:featureFlag="android.os.vibrator.vendor_vibration_effects" /> + <!-- @SystemApi Allows access to the vibrator state. <p>Protection level: signature @hide @@ -4454,6 +4463,18 @@ android:description="@string/permdesc_hideOverlayWindows" android:protectionLevel="normal" /> + <!-- Allows an app to enter Picture-in-Picture mode when the user is not explicitly requesting + it. This includes using {@link PictureInPictureParams.Builder#setAutoEnterEnabled} as well + as lifecycle methods such as {@link Activity#onUserLeaveHint} and {@link Activity#onPause} + to enter PiP when the user leaves the app. + This permission should only be used for certain PiP + <a href="{@docRoot}training/tv/get-started/multitasking#usage-types">usage types</a>. + @FlaggedApi(android.app.Flags.FLAG_ENABLE_TV_IMPLICIT_ENTER_PIP_RESTRICTION) + --> + <permission android:name="android.permission.TV_IMPLICIT_ENTER_PIP" + android:protectionLevel="normal" + android:featureFlag="android.app.enable_tv_implicit_enter_pip_restriction" /> + <!-- ================================== --> <!-- Permissions affecting the system wallpaper --> <!-- ================================== --> @@ -4960,6 +4981,27 @@ <permission android:name="android.permission.PROVIDE_REMOTE_CREDENTIALS" android:protectionLevel="signature|privileged|role" /> + <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_SETTINGS_CATALYST) + Allows an application to access the Settings Preference services to read settings exposed + by the system Settings app and system apps that contribute settings surfaced by the + Settings app. + <p>This allows the calling application to read settings values through the host + application, agnostic of underlying storage. --> + <permission android:name="android.permission.READ_SYSTEM_PREFERENCES" + android:protectionLevel="signature|privileged|role" + android:featureFlag="com.android.settingslib.flags.settings_catalyst" /> + + <!-- @FlaggedApi(com.android.settingslib.flags.Flags.FLAG_SETTINGS_CATALYST) + Allows an application to access the Settings Preference services to write settings + values exposed by the system Settings app and system apps that contribute settings surfaced + in the Settings app. + <p>This allows the calling application to write settings values + through the host application, agnostic of underlying storage. + <p>Protection Level: signature|privileged|appop - appop to be added in followup --> + <permission android:name="android.permission.WRITE_SYSTEM_PREFERENCES" + android:protectionLevel="signature|privileged" + android:featureFlag="com.android.settingslib.flags.settings_catalyst" /> + <!-- ========================================= --> <!-- Permissions for special development tools --> <!-- ========================================= --> diff --git a/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt new file mode 100644 index 000000000000..a6de611cc077 --- /dev/null +++ b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt @@ -0,0 +1,472 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.hardware.display + +import android.hardware.display.DisplayTopology.TreeNode.POSITION_BOTTOM +import android.hardware.display.DisplayTopology.TreeNode.POSITION_TOP +import android.hardware.display.DisplayTopology.TreeNode.POSITION_RIGHT +import android.view.Display +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class DisplayTopologyTest { + private var topology = DisplayTopology() + + @Test + fun addOneDisplay() { + val displayId = 1 + val width = 800f + val height = 600f + + topology.addDisplay(displayId, width, height) + + assertThat(topology.primaryDisplayId).isEqualTo(displayId) + + val display = topology.root!! + assertThat(display.displayId).isEqualTo(displayId) + assertThat(display.width).isEqualTo(width) + assertThat(display.height).isEqualTo(height) + assertThat(display.children).isEmpty() + } + + @Test + fun addTwoDisplays() { + val displayId1 = 1 + val width1 = 800f + val height1 = 600f + + val displayId2 = 2 + val width2 = 1000f + val height2 = 1500f + + topology.addDisplay(displayId1, width1, height1) + topology.addDisplay(displayId2, width2, height2) + + assertThat(topology.primaryDisplayId).isEqualTo(displayId1) + + val display1 = topology.root!! + assertThat(display1.displayId).isEqualTo(displayId1) + assertThat(display1.width).isEqualTo(width1) + assertThat(display1.height).isEqualTo(height1) + assertThat(display1.children).hasSize(1) + + val display2 = display1.children[0] + assertThat(display2.displayId).isEqualTo(displayId2) + assertThat(display2.width).isEqualTo(width2) + assertThat(display2.height).isEqualTo(height2) + assertThat(display2.children).isEmpty() + assertThat(display2.position).isEqualTo(POSITION_TOP) + assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2) + } + + @Test + fun addManyDisplays() { + val displayId1 = 1 + val width1 = 800f + val height1 = 600f + + val displayId2 = 2 + val width2 = 1000f + val height2 = 1500f + + topology.addDisplay(displayId1, width1, height1) + topology.addDisplay(displayId2, width2, height2) + + val noOfDisplays = 30 + for (i in 3..noOfDisplays) { + topology.addDisplay(/* displayId= */ i, width1, height1) + } + + assertThat(topology.primaryDisplayId).isEqualTo(displayId1) + + val display1 = topology.root!! + assertThat(display1.displayId).isEqualTo(displayId1) + assertThat(display1.width).isEqualTo(width1) + assertThat(display1.height).isEqualTo(height1) + assertThat(display1.children).hasSize(1) + + val display2 = display1.children[0] + assertThat(display2.displayId).isEqualTo(displayId2) + assertThat(display2.width).isEqualTo(width2) + assertThat(display2.height).isEqualTo(height2) + assertThat(display2.children).hasSize(1) + assertThat(display2.position).isEqualTo(POSITION_TOP) + assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2) + + var display = display2 + for (i in 3..noOfDisplays) { + display = display.children[0] + assertThat(display.displayId).isEqualTo(i) + assertThat(display.width).isEqualTo(width1) + assertThat(display.height).isEqualTo(height1) + // The last display should have no children + assertThat(display.children).hasSize(if (i < noOfDisplays) 1 else 0) + assertThat(display.position).isEqualTo(POSITION_RIGHT) + assertThat(display.offset).isEqualTo(0) + } + } + + @Test + fun removeDisplays() { + val displayId1 = 1 + val width1 = 800f + val height1 = 600f + + val displayId2 = 2 + val width2 = 1000f + val height2 = 1500f + + topology.addDisplay(displayId1, width1, height1) + topology.addDisplay(displayId2, width2, height2) + + val noOfDisplays = 30 + for (i in 3..noOfDisplays) { + topology.addDisplay(/* displayId= */ i, width1, height1) + } + + var removedDisplays = arrayOf(20) + topology.removeDisplay(20) + + assertThat(topology.primaryDisplayId).isEqualTo(displayId1) + + var display1 = topology.root!! + assertThat(display1.displayId).isEqualTo(displayId1) + assertThat(display1.width).isEqualTo(width1) + assertThat(display1.height).isEqualTo(height1) + assertThat(display1.children).hasSize(1) + + var display2 = display1.children[0] + assertThat(display2.displayId).isEqualTo(displayId2) + assertThat(display2.width).isEqualTo(width2) + assertThat(display2.height).isEqualTo(height2) + assertThat(display2.children).hasSize(1) + assertThat(display2.position).isEqualTo(POSITION_TOP) + assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2) + + var display = display2 + for (i in 3..noOfDisplays) { + if (i in removedDisplays) { + continue + } + display = display.children[0] + assertThat(display.displayId).isEqualTo(i) + assertThat(display.width).isEqualTo(width1) + assertThat(display.height).isEqualTo(height1) + // The last display should have no children + assertThat(display.children).hasSize(if (i < noOfDisplays) 1 else 0) + assertThat(display.position).isEqualTo(POSITION_RIGHT) + assertThat(display.offset).isEqualTo(0) + } + + topology.removeDisplay(22) + removedDisplays += 22 + topology.removeDisplay(23) + removedDisplays += 23 + topology.removeDisplay(25) + removedDisplays += 25 + + assertThat(topology.primaryDisplayId).isEqualTo(displayId1) + + display1 = topology.root!! + assertThat(display1.displayId).isEqualTo(displayId1) + assertThat(display1.width).isEqualTo(width1) + assertThat(display1.height).isEqualTo(height1) + assertThat(display1.children).hasSize(1) + + display2 = display1.children[0] + assertThat(display2.displayId).isEqualTo(displayId2) + assertThat(display2.width).isEqualTo(width2) + assertThat(display2.height).isEqualTo(height2) + assertThat(display2.children).hasSize(1) + assertThat(display2.position).isEqualTo(POSITION_TOP) + assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2) + + display = display2 + for (i in 3..noOfDisplays) { + if (i in removedDisplays) { + continue + } + display = display.children[0] + assertThat(display.displayId).isEqualTo(i) + assertThat(display.width).isEqualTo(width1) + assertThat(display.height).isEqualTo(height1) + // The last display should have no children + assertThat(display.children).hasSize(if (i < noOfDisplays) 1 else 0) + assertThat(display.position).isEqualTo(POSITION_RIGHT) + assertThat(display.offset).isEqualTo(0) + } + } + + @Test + fun removeAllDisplays() { + val displayId = 1 + val width = 800f + val height = 600f + + topology.addDisplay(displayId, width, height) + topology.removeDisplay(displayId) + + assertThat(topology.primaryDisplayId).isEqualTo(Display.INVALID_DISPLAY) + assertThat(topology.root).isNull() + } + + @Test + fun removeDisplayThatDoesNotExist() { + val displayId = 1 + val width = 800f + val height = 600f + + topology.addDisplay(displayId, width, height) + topology.removeDisplay(3) + + assertThat(topology.primaryDisplayId).isEqualTo(displayId) + + val display = topology.root!! + assertThat(display.displayId).isEqualTo(displayId) + assertThat(display.width).isEqualTo(width) + assertThat(display.height).isEqualTo(height) + assertThat(display.children).isEmpty() + } + + @Test + fun removePrimaryDisplay() { + val displayId1 = 1 + val displayId2 = 2 + val width = 800f + val height = 600f + + topology = DisplayTopology(/* root= */ null, displayId2) + topology.addDisplay(displayId1, width, height) + topology.addDisplay(displayId2, width, height) + topology.removeDisplay(displayId2) + + assertThat(topology.primaryDisplayId).isEqualTo(displayId1) + val display = topology.root!! + assertThat(display.displayId).isEqualTo(displayId1) + assertThat(display.width).isEqualTo(width) + assertThat(display.height).isEqualTo(height) + assertThat(display.children).isEmpty() + } + + @Test + fun normalization_noOverlaps_leavesTopologyUnchanged() { + val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, + /* height= */ 600f, /* position= */ 0, /* offset= */ 0f) + + val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 0f) + display1.addChild(display2) + + val primaryDisplayId = 3 + val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 400f) + display1.addChild(display3) + + val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f, + /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) + display2.addChild(display4) + + topology = DisplayTopology(display1, primaryDisplayId) + topology.normalize() + + assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId) + + val actualDisplay1 = topology.root!! + assertThat(actualDisplay1.displayId).isEqualTo(1) + assertThat(actualDisplay1.width).isEqualTo(200f) + assertThat(actualDisplay1.height).isEqualTo(600f) + assertThat(actualDisplay1.children).hasSize(2) + + val actualDisplay2 = actualDisplay1.children[0] + assertThat(actualDisplay2.displayId).isEqualTo(2) + assertThat(actualDisplay2.width).isEqualTo(600f) + assertThat(actualDisplay2.height).isEqualTo(200f) + assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay2.offset).isEqualTo(0f) + assertThat(actualDisplay2.children).hasSize(1) + + val actualDisplay3 = actualDisplay1.children[1] + assertThat(actualDisplay3.displayId).isEqualTo(3) + assertThat(actualDisplay3.width).isEqualTo(600f) + assertThat(actualDisplay3.height).isEqualTo(200f) + assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay3.offset).isEqualTo(400f) + assertThat(actualDisplay3.children).isEmpty() + + val actualDisplay4 = actualDisplay2.children[0] + assertThat(actualDisplay4.displayId).isEqualTo(4) + assertThat(actualDisplay4.width).isEqualTo(200f) + assertThat(actualDisplay4.height).isEqualTo(600f) + assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay4.offset).isEqualTo(0f) + assertThat(actualDisplay4.children).isEmpty() + } + + @Test + fun normalization_moveDisplayWithoutReparenting() { + val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, + /* height= */ 600f, /* position= */ 0, /* offset= */ 0f) + + val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 200f, + /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) + display1.addChild(display2) + + val primaryDisplayId = 3 + val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 10f) + display1.addChild(display3) + + val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f, + /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) + display2.addChild(display4) + + topology = DisplayTopology(display1, primaryDisplayId) + // Display 3 becomes a child of display 2. Display 4 gets moved without changing its parent. + topology.normalize() + + assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId) + + val actualDisplay1 = topology.root!! + assertThat(actualDisplay1.displayId).isEqualTo(1) + assertThat(actualDisplay1.width).isEqualTo(200f) + assertThat(actualDisplay1.height).isEqualTo(600f) + assertThat(actualDisplay1.children).hasSize(1) + + val actualDisplay2 = actualDisplay1.children[0] + assertThat(actualDisplay2.displayId).isEqualTo(2) + assertThat(actualDisplay2.width).isEqualTo(200f) + assertThat(actualDisplay2.height).isEqualTo(600f) + assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay2.offset).isEqualTo(0f) + assertThat(actualDisplay2.children).hasSize(2) + + val actualDisplay3 = actualDisplay2.children[1] + assertThat(actualDisplay3.displayId).isEqualTo(3) + assertThat(actualDisplay3.width).isEqualTo(600f) + assertThat(actualDisplay3.height).isEqualTo(200f) + assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay3.offset).isEqualTo(10f) + assertThat(actualDisplay3.children).isEmpty() + + val actualDisplay4 = actualDisplay2.children[0] + assertThat(actualDisplay4.displayId).isEqualTo(4) + assertThat(actualDisplay4.width).isEqualTo(200f) + assertThat(actualDisplay4.height).isEqualTo(600f) + assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay4.offset).isEqualTo(210f) + assertThat(actualDisplay4.children).isEmpty() + } + + @Test + fun normalization_moveDisplayWithoutReparenting_offsetOutOfBounds() { + val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, + /* height= */ 50f, /* position= */ 0, /* offset= */ 0f) + + val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 0f) + display1.addChild(display2) + + val primaryDisplayId = 3 + val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 10f) + display1.addChild(display3) + + topology = DisplayTopology(display1, primaryDisplayId) + // Display 3 gets moved and its left side is still on the same line as the right side + // of Display 1, but it no longer touches it (the offset is out of bounds), so Display 2 + // becomes its new parent. + topology.normalize() + + assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId) + + val actualDisplay1 = topology.root!! + assertThat(actualDisplay1.displayId).isEqualTo(1) + assertThat(actualDisplay1.width).isEqualTo(200f) + assertThat(actualDisplay1.height).isEqualTo(50f) + assertThat(actualDisplay1.children).hasSize(1) + + val actualDisplay2 = actualDisplay1.children[0] + assertThat(actualDisplay2.displayId).isEqualTo(2) + assertThat(actualDisplay2.width).isEqualTo(600f) + assertThat(actualDisplay2.height).isEqualTo(200f) + assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay2.offset).isEqualTo(0f) + assertThat(actualDisplay2.children).hasSize(1) + + val actualDisplay3 = actualDisplay2.children[0] + assertThat(actualDisplay3.displayId).isEqualTo(3) + assertThat(actualDisplay3.width).isEqualTo(600f) + assertThat(actualDisplay3.height).isEqualTo(200f) + assertThat(actualDisplay3.position).isEqualTo(POSITION_BOTTOM) + assertThat(actualDisplay3.offset).isEqualTo(0f) + assertThat(actualDisplay3.children).isEmpty() + } + + @Test + fun normalization_moveAndReparentDisplay() { + val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, + /* height= */ 600f, /* position= */ 0, /* offset= */ 0f) + + val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 200f, + /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) + display1.addChild(display2) + + val primaryDisplayId = 3 + val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f, + /* height= */ 200f, POSITION_RIGHT, /* offset= */ 400f) + display1.addChild(display3) + + val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f, + /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) + display2.addChild(display4) + + topology = DisplayTopology(display1, primaryDisplayId) + topology.normalize() + + assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId) + + val actualDisplay1 = topology.root!! + assertThat(actualDisplay1.displayId).isEqualTo(1) + assertThat(actualDisplay1.width).isEqualTo(200f) + assertThat(actualDisplay1.height).isEqualTo(600f) + assertThat(actualDisplay1.children).hasSize(1) + + val actualDisplay2 = actualDisplay1.children[0] + assertThat(actualDisplay2.displayId).isEqualTo(2) + assertThat(actualDisplay2.width).isEqualTo(200f) + assertThat(actualDisplay2.height).isEqualTo(600f) + assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay2.offset).isEqualTo(0f) + assertThat(actualDisplay2.children).hasSize(1) + + val actualDisplay3 = actualDisplay2.children[0] + assertThat(actualDisplay3.displayId).isEqualTo(3) + assertThat(actualDisplay3.width).isEqualTo(600f) + assertThat(actualDisplay3.height).isEqualTo(200f) + assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay3.offset).isEqualTo(400f) + assertThat(actualDisplay3.children).hasSize(1) + + val actualDisplay4 = actualDisplay3.children[0] + assertThat(actualDisplay4.displayId).isEqualTo(4) + assertThat(actualDisplay4.width).isEqualTo(200f) + assertThat(actualDisplay4.height).isEqualTo(600f) + assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT) + assertThat(actualDisplay4.offset).isEqualTo(-400f) + assertThat(actualDisplay4.children).isEmpty() + } +}
\ No newline at end of file diff --git a/core/tests/coretests/src/android/view/DisplayInfoTest.java b/core/tests/coretests/src/android/view/DisplayInfoTest.java index 4c5b7e508e34..8932cf1ba552 100644 --- a/core/tests/coretests/src/android/view/DisplayInfoTest.java +++ b/core/tests/coretests/src/android/view/DisplayInfoTest.java @@ -78,6 +78,23 @@ public class DisplayInfoTest { } @Test + public void testRefreshRateOverride_keepsDisplyInfosEqualWhenOverrideIsSame() { + Display.Mode mode = new Display.Mode( + /*modeId=*/1, /*width=*/1000, /*height=*/1000, /*refreshRate=*/120); + DisplayInfo displayInfo1 = new DisplayInfo(); + setSupportedMode(displayInfo1, mode); + displayInfo1.renderFrameRate = 60; + displayInfo1.refreshRateOverride = 30; + + DisplayInfo displayInfo2 = new DisplayInfo(); + setSupportedMode(displayInfo2, mode); + displayInfo2.renderFrameRate = 30; + displayInfo2.refreshRateOverride = 30; + + assertTrue(displayInfo1.equals(displayInfo2)); + } + + @Test public void testRefreshRateOverride_makeDisplayInfosDifferent() { Display.Mode mode = new Display.Mode( /*modeId=*/1, /*width=*/1000, /*height=*/1000, /*refreshRate=*/120); diff --git a/core/tests/vibrator/src/android/os/VibratorTest.java b/core/tests/vibrator/src/android/os/VibratorTest.java index 6210a00a5940..09bfadbf56a4 100644 --- a/core/tests/vibrator/src/android/os/VibratorTest.java +++ b/core/tests/vibrator/src/android/os/VibratorTest.java @@ -110,8 +110,9 @@ public class VibratorTest { @Test public void onVibratorStateChanged_noVibrator_registersNoListenerToVibratorManager() { + int[] vibratorIds = new int[0]; VibratorManager mockVibratorManager = mock(VibratorManager.class); - when(mockVibratorManager.getVibratorIds()).thenReturn(new int[0]); + when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds); Vibrator.OnVibratorStateChangedListener mockListener = mock(Vibrator.OnVibratorStateChangedListener.class); @@ -119,7 +120,7 @@ public class VibratorTest { new SystemVibrator.MultiVibratorStateListener( mTestLooper.getNewExecutor(), mockListener); - multiVibratorListener.register(mockVibratorManager); + multiVibratorListener.register(mockVibratorManager, vibratorIds); // Never tries to register a listener to an individual vibrator. assertFalse(multiVibratorListener.hasRegisteredListeners()); @@ -128,8 +129,9 @@ public class VibratorTest { @Test public void onVibratorStateChanged_singleVibrator_forwardsAllCallbacks() { + int[] vibratorIds = new int[] { 1 }; VibratorManager mockVibratorManager = mock(VibratorManager.class); - when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1 }); + when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds); when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance()); Vibrator.OnVibratorStateChangedListener mockListener = @@ -138,7 +140,7 @@ public class VibratorTest { new SystemVibrator.MultiVibratorStateListener( mTestLooper.getNewExecutor(), mockListener); - multiVibratorListener.register(mockVibratorManager); + multiVibratorListener.register(mockVibratorManager, vibratorIds); assertTrue(multiVibratorListener.hasRegisteredListeners()); multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false); @@ -156,8 +158,9 @@ public class VibratorTest { @Test public void onVibratorStateChanged_multipleVibrators_triggersOnlyWhenAllVibratorsInitialized() { + int[] vibratorIds = new int[] { 1, 2 }; VibratorManager mockVibratorManager = mock(VibratorManager.class); - when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1, 2 }); + when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds); when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance()); Vibrator.OnVibratorStateChangedListener mockListener = @@ -166,7 +169,7 @@ public class VibratorTest { new SystemVibrator.MultiVibratorStateListener( mTestLooper.getNewExecutor(), mockListener); - multiVibratorListener.register(mockVibratorManager); + multiVibratorListener.register(mockVibratorManager, vibratorIds); assertTrue(multiVibratorListener.hasRegisteredListeners()); multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false); @@ -181,8 +184,9 @@ public class VibratorTest { @Test public void onVibratorStateChanged_multipleVibrators_stateChangeIsDeduped() { + int[] vibratorIds = new int[] { 1, 2 }; VibratorManager mockVibratorManager = mock(VibratorManager.class); - when(mockVibratorManager.getVibratorIds()).thenReturn(new int[] { 1, 2 }); + when(mockVibratorManager.getVibratorIds()).thenReturn(vibratorIds); when(mockVibratorManager.getVibrator(anyInt())).thenReturn(NullVibrator.getInstance()); Vibrator.OnVibratorStateChangedListener mockListener = @@ -191,7 +195,7 @@ public class VibratorTest { new SystemVibrator.MultiVibratorStateListener( mTestLooper.getNewExecutor(), mockListener); - multiVibratorListener.register(mockVibratorManager); + multiVibratorListener.register(mockVibratorManager, vibratorIds); assertTrue(multiVibratorListener.hasRegisteredListeners()); multiVibratorListener.onVibrating(/* vibratorIdx= */ 0, /* vibrating= */ false); // none diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 56e55df3f27c..7ced809d2a3a 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -402,6 +402,9 @@ applications that come with the platform <permission name="android.permission.SHOW_CUSTOMIZED_RESOLVER"/> <!-- Permission required for access VIBRATOR_STATE. --> <permission name="android.permission.ACCESS_VIBRATOR_STATE"/> + <!-- Permission required for vendor vibration effects and sessions. --> + <permission name="android.permission.VIBRATE_VENDOR_EFFECTS"/> + <permission name="android.permission.START_VIBRATION_SESSIONS"/> <!-- Permission required for UsageStatsTest CTS test. --> <permission name="android.permission.MANAGE_NOTIFICATIONS"/> <!-- Permission required for CompanionDeviceManager CTS test. --> diff --git a/keystore/java/android/security/keystore/KeyStoreManager.java b/keystore/java/android/security/keystore/KeyStoreManager.java index 197aaba4bcb5..e6091c1da8a5 100644 --- a/keystore/java/android/security/keystore/KeyStoreManager.java +++ b/keystore/java/android/security/keystore/KeyStoreManager.java @@ -49,7 +49,7 @@ import java.util.List; */ @FlaggedApi(android.security.Flags.FLAG_KEYSTORE_GRANT_API) @SystemService(Context.KEYSTORE_SERVICE) -public class KeyStoreManager { +public final class KeyStoreManager { private static final String TAG = "KeyStoreManager"; private static final Object sInstanceLock = new Object(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index cefcb757690f..01c680dc8325 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -205,11 +205,6 @@ class DesktopMixedTransitionHandler( finishTransaction: SurfaceControl.Transaction, finishCallback: TransitionFinishCallback, ): Boolean { - val launchChange = findDesktopTaskChange(info, pending.launchingTask) - if (launchChange == null) { - logV("No launch Change, returning") - return false - } // Check if there's also an immersive change during this launch. val immersiveExitChange = pending.exitingImmersiveTask?.let { exitingTask -> findDesktopTaskChange(info, exitingTask) @@ -217,6 +212,13 @@ class DesktopMixedTransitionHandler( val minimizeChange = pending.minimizingTask?.let { minimizingTask -> findDesktopTaskChange(info, minimizingTask) } + val launchChange = findDesktopTaskChange(info, pending.launchingTask) + if (launchChange == null) { + check(minimizeChange == null) + check(immersiveExitChange == null) + logV("No launch Change, returning") + return false + } var subAnimationCount = -1 var combinedWct: WindowContainerTransaction? = null diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index b08a86ee8f46..bd65b2ecb76a 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -17,6 +17,7 @@ package android.media; import static android.media.codec.Flags.FLAG_IN_PROCESS_SW_AUDIO_CODEC; +import static android.media.codec.Flags.FLAG_NUM_INPUT_SLOTS; import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST; import static android.media.codec.Flags.FLAG_APV_SUPPORT; @@ -1777,6 +1778,17 @@ public final class MediaFormat { public static final String KEY_SECURITY_MODEL = "security-model"; /** + * A key describing the number of slots used in the codec. When present in input format, + * the associated value indicates the number of input slots. The entry is set by the codec + * if configured with (@link MediaCodec#CONFIGURE_FLAG_BLOCK_MODEL), and will be ignored if set + * by the application. + * <p> + * The associated value is an integer. + */ + @FlaggedApi(FLAG_NUM_INPUT_SLOTS) + public static final String KEY_NUM_SLOTS = "num-slots"; + + /** * QpOffsetRect constitutes the metadata required for encoding a region of interest in an * image or a video frame. The region of interest is represented by a rectangle. The four * integer coordinates of the rectangle are stored in fields left, top, right, bottom. diff --git a/media/java/android/media/audio/common/AidlConversion.java b/media/java/android/media/audio/common/AidlConversion.java index c1d73f9033cf..8521d1c472a8 100644 --- a/media/java/android/media/audio/common/AidlConversion.java +++ b/media/java/android/media/audio/common/AidlConversion.java @@ -705,6 +705,10 @@ public class AidlConversion { aidl.type = AudioDeviceType.OUT_BROADCAST; aidl.connection = AudioDeviceDescription.CONNECTION_BT_LE; break; + case AudioSystem.DEVICE_OUT_MULTICHANNEL_GROUP: + aidl.type = AudioDeviceType.OUT_MULTICHANNEL_GROUP; + aidl.connection = AudioDeviceDescription.CONNECTION_VIRTUAL; + break; case AudioSystem.DEVICE_IN_BUILTIN_MIC: aidl.type = AudioDeviceType.IN_MICROPHONE; break; diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index 90b4aba690d0..52a21e241ba8 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -79,7 +79,7 @@ flag { flag { name: "update_client_profile_priority" - namespace: "media" + namespace: "media_solutions" description : "Feature flag to add updateResourcePriority api to MediaCas" bug: "300565729" } diff --git a/media/tests/aidltests/src/com/android/media/AidlConversionUnitTests.java b/media/tests/aidltests/src/com/android/media/AidlConversionUnitTests.java index 09573909c288..d9a1221e529c 100644 --- a/media/tests/aidltests/src/com/android/media/AidlConversionUnitTests.java +++ b/media/tests/aidltests/src/com/android/media/AidlConversionUnitTests.java @@ -18,6 +18,7 @@ package android.media.audio.common; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -504,6 +505,27 @@ public final class AidlConversionUnitTests { assertEquals(AudioDeviceType.OUT_DEVICE, port.ext.getDevice().device.type.type); } + @Test + public void testAudioDeviceDescriptionConversion() { + for (int nativeDeviceType : AudioSystem.DEVICE_OUT_ALL_SET) { + assertNotEquals( + AidlConversion.api2aidl_NativeType_AudioDeviceDescription(nativeDeviceType) + .type, + AudioDeviceType.NONE); + } + + for (int nativeDeviceType : AudioSystem.DEVICE_IN_ALL_SET) { + if (nativeDeviceType == AudioSystem.DEVICE_IN_COMMUNICATION + || nativeDeviceType == AudioSystem.DEVICE_IN_AMBIENT) { + continue; + } + assertNotEquals( + AidlConversion.api2aidl_NativeType_AudioDeviceDescription(nativeDeviceType) + .type, + AudioDeviceType.NONE); + } + } + private static AudioFormatDescription createPcm16FormatAidl() { final AudioFormatDescription aidl = new AudioFormatDescription(); aidl.type = AudioFormatType.PCM; diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index e1418678a9f8..b2dcb7fa4f53 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -61,7 +61,7 @@ android_library { "SettingsLibUtils", "SettingsLibZeroStatePreference", "settingslib_media_flags_lib", - "settingslib_flags_lib", + "aconfig_settingslib_flags_java_lib", ], plugins: ["androidx.room_room-compiler-plugin"], @@ -107,20 +107,6 @@ java_aconfig_library { aconfig_declarations: "settingslib_media_flags", } -aconfig_declarations { - name: "settingslib_flags", - package: "com.android.settingslib.flags", - container: "system", - srcs: [ - "aconfig/settingslib.aconfig", - ], -} - -java_aconfig_library { - name: "settingslib_flags_lib", - aconfig_declarations: "settingslib_flags", -} - soong_config_module_type { name: "avatar_picker_java_defaults", module_type: "java_defaults", diff --git a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java index 979ff96be3f7..993555e78bea 100644 --- a/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java +++ b/packages/SettingsLib/ButtonPreference/src/com/android/settingslib/widget/ButtonPreference.java @@ -37,7 +37,7 @@ import com.google.android.material.button.MaterialButton; /** * A preference handled a button */ -public class ButtonPreference extends Preference { +public class ButtonPreference extends Preference implements GroupSectionDividerMixin { enum ButtonStyle { FILLED_NORMAL(0, 0, R.layout.settingslib_expressive_button_filled), diff --git a/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java b/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java index d60290ed91ef..37f47543c536 100644 --- a/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java +++ b/packages/SettingsLib/FooterPreference/src/com/android/settingslib/widget/FooterPreference.java @@ -43,7 +43,7 @@ import java.net.URISyntaxException; * A custom preference acting as "footer" of a page. It has a field for icon and text. It is added * to screen as the last preference. */ -public class FooterPreference extends Preference { +public class FooterPreference extends Preference implements GroupSectionDividerMixin { private static final String TAG = "FooterPreference"; public static final String KEY_FOOTER = "footer_preference"; diff --git a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt index c1578ef69635..1f8cfb5e432e 100644 --- a/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt +++ b/packages/SettingsLib/StatusBannerPreference/src/com/android/settingslib/widget/StatusBannerPreference.kt @@ -34,7 +34,7 @@ class StatusBannerPreference @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 -) : Preference(context, attrs, defStyleAttr, defStyleRes) { +) : Preference(context, attrs, defStyleAttr, defStyleRes), GroupSectionDividerMixin { enum class BannerStatus { GENERIC, diff --git a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt index 5be56f8ebc86..9764e64b8509 100644 --- a/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt +++ b/packages/SettingsLib/TopIntroPreference/src/com/android/settingslib/widget/TopIntroPreference.kt @@ -31,7 +31,7 @@ open class TopIntroPreference @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 -) : Preference(context, attrs, defStyleAttr, defStyleRes) { +) : Preference(context, attrs, defStyleAttr, defStyleRes), GroupSectionDividerMixin { private var isCollapsable: Boolean = false private var minLines: Int = 2 diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 2c8c261fa8f8..24b91f9ab436 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -263,6 +263,8 @@ <uses-permission android:name="android.permission.MANAGE_APP_OPS_MODES" /> <uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.ACCESS_VIBRATOR_STATE" /> + <uses-permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" /> + <uses-permission android:name="android.permission.START_VIBRATION_SESSIONS" /> <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> <uses-permission android:name="android.permission.START_TASKS_FROM_RECENTS" /> <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" /> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 87ea2a7ab2ae..5eae6d3e43fe 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -414,6 +414,17 @@ flag { } flag { + name: "status_bar_auto_start_screen_record_chip" + namespace: "systemui" + description: "When screen recording, use the specified start time to update the screen record " + "chip state instead of waiting for an official 'recording started' signal" + bug: "366448907" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "status_bar_use_repos_for_call_chip" namespace: "systemui" description: "Use repositories as the source of truth for call notifications shown as a chip in" @@ -1676,6 +1687,16 @@ flag { } flag { + name: "show_toast_when_app_control_brightness" + namespace: "systemui" + description: "Showing the warning toast if the current running app window has controlled the brightness value." + bug: "363225340" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "home_controls_dream_hsum" namespace: "systemui" description: "Enables the home controls dream in HSUM" diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt index 00d905652987..300bdf2ffb01 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt @@ -48,7 +48,7 @@ class ViewHierarchyAnimator { Bound.LEFT to createViewProperty(Bound.LEFT), Bound.TOP to createViewProperty(Bound.TOP), Bound.RIGHT to createViewProperty(Bound.RIGHT), - Bound.BOTTOM to createViewProperty(Bound.BOTTOM) + Bound.BOTTOM to createViewProperty(Bound.BOTTOM), ) private fun createViewProperty(bound: Bound): IntProperty<View> { @@ -89,7 +89,7 @@ class ViewHierarchyAnimator { interpolator: Interpolator = DEFAULT_INTERPOLATOR, duration: Long = DEFAULT_DURATION, animateChildren: Boolean = true, - excludedViews: Set<View> = emptySet() + excludedViews: Set<View> = emptySet(), ): Boolean { return animate( rootView, @@ -97,7 +97,7 @@ class ViewHierarchyAnimator { duration, ephemeral = false, animateChildren = animateChildren, - excludedViews = excludedViews + excludedViews = excludedViews, ) } @@ -111,7 +111,7 @@ class ViewHierarchyAnimator { interpolator: Interpolator = DEFAULT_INTERPOLATOR, duration: Long = DEFAULT_DURATION, animateChildren: Boolean = true, - excludedViews: Set<View> = emptySet() + excludedViews: Set<View> = emptySet(), ): Boolean { return animate( rootView, @@ -119,7 +119,7 @@ class ViewHierarchyAnimator { duration, ephemeral = true, animateChildren = animateChildren, - excludedViews = excludedViews + excludedViews = excludedViews, ) } @@ -129,7 +129,7 @@ class ViewHierarchyAnimator { duration: Long, ephemeral: Boolean, animateChildren: Boolean, - excludedViews: Set<View> = emptySet() + excludedViews: Set<View> = emptySet(), ): Boolean { if ( !occupiesSpace( @@ -137,7 +137,7 @@ class ViewHierarchyAnimator { rootView.left, rootView.top, rootView.right, - rootView.bottom + rootView.bottom, ) ) { return false @@ -149,7 +149,7 @@ class ViewHierarchyAnimator { listener, recursive = true, animateChildren = animateChildren, - excludedViews = excludedViews + excludedViews = excludedViews, ) return true } @@ -164,7 +164,7 @@ class ViewHierarchyAnimator { private fun createUpdateListener( interpolator: Interpolator, duration: Long, - ephemeral: Boolean + ephemeral: Boolean, ): View.OnLayoutChangeListener { return createListener(interpolator, duration, ephemeral) } @@ -196,9 +196,9 @@ class ViewHierarchyAnimator { * * @param includeFadeIn true if the animator should also fade in the view and child views. * @param fadeInInterpolator the interpolator to use when fading in the view. Unused if - * [includeFadeIn] is false. - * @param onAnimationEnd an optional runnable that will be run once the animation - * finishes successfully. Will not be run if the animation is cancelled. + * [includeFadeIn] is false. + * @param onAnimationEnd an optional runnable that will be run once the animation finishes, + * regardless of whether the animation is cancelled or finishes successfully. */ @JvmOverloads fun animateAddition( @@ -217,7 +217,7 @@ class ViewHierarchyAnimator { rootView.left, rootView.top, rootView.right, - rootView.bottom + rootView.bottom, ) ) { return false @@ -241,7 +241,10 @@ class ViewHierarchyAnimator { // First, fade in the container view val containerDuration = duration / 6 createAndStartFadeInAnimator( - rootView, containerDuration, startDelay = 0, interpolator = fadeInInterpolator + rootView, + containerDuration, + startDelay = 0, + interpolator = fadeInInterpolator, ) // Then, fade in the child views @@ -253,7 +256,7 @@ class ViewHierarchyAnimator { childDuration, // Wait until the container fades in before fading in the children startDelay = containerDuration, - interpolator = fadeInInterpolator + interpolator = fadeInInterpolator, ) } // For now, we don't recursively fade in additional sub views (e.g. grandchild @@ -264,7 +267,7 @@ class ViewHierarchyAnimator { rootView, duration / 2, startDelay = 0, - interpolator = fadeInInterpolator + interpolator = fadeInInterpolator, ) } @@ -323,7 +326,7 @@ class ViewHierarchyAnimator { previousLeft: Int, previousTop: Int, previousRight: Int, - previousBottom: Int + previousBottom: Int, ) { if (view == null) return @@ -353,14 +356,14 @@ class ViewHierarchyAnimator { startTop, startRight, startBottom, - ignorePreviousValues + ignorePreviousValues, ) val endValues = mapOf( Bound.LEFT to left, Bound.TOP to top, Bound.RIGHT to right, - Bound.BOTTOM to bottom + Bound.BOTTOM to bottom, ) val boundsToAnimate = mutableSetOf<Bound>() @@ -396,8 +399,8 @@ class ViewHierarchyAnimator { * added on the side(s) of the [destination], the translation of those margins can be * included by specifying [includeMargins]. * - * @param onAnimationEnd an optional runnable that will be run once the animation finishes - * successfully. Will not be run if the animation is cancelled. + * @param onAnimationEnd an optional runnable that will be run once the animation finishes, + * regardless of whether the animation is cancelled or finishes successfully. */ @JvmOverloads fun animateRemoval( @@ -414,7 +417,7 @@ class ViewHierarchyAnimator { rootView.left, rootView.top, rootView.right, - rootView.bottom + rootView.bottom, ) ) { return false @@ -458,7 +461,7 @@ class ViewHierarchyAnimator { Bound.LEFT to rootView.left, Bound.TOP to rootView.top, Bound.RIGHT to rootView.right, - Bound.BOTTOM to rootView.bottom + Bound.BOTTOM to rootView.bottom, ) val endValues = processEndValuesForRemoval( @@ -550,7 +553,7 @@ class ViewHierarchyAnimator { destination: Hotspot, endValues: Map<Bound, Int>, interpolator: Interpolator, - duration: Long + duration: Long, ) { for (i in 0 until rootView.childCount) { val child = rootView.getChildAt(i) @@ -559,7 +562,7 @@ class ViewHierarchyAnimator { Bound.LEFT to child.left, Bound.TOP to child.top, Bound.RIGHT to child.right, - Bound.BOTTOM to child.bottom + Bound.BOTTOM to child.bottom, ) val childEndValues = processChildEndValuesForRemoval( @@ -569,7 +572,7 @@ class ViewHierarchyAnimator { child.right, child.bottom, endValues.getValue(Bound.RIGHT) - endValues.getValue(Bound.LEFT), - endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP) + endValues.getValue(Bound.BOTTOM) - endValues.getValue(Bound.TOP), ) val boundsToAnimate = mutableSetOf<Bound>() @@ -587,7 +590,7 @@ class ViewHierarchyAnimator { childEndValues, interpolator, duration, - ephemeral = true + ephemeral = true, ) } } @@ -601,7 +604,7 @@ class ViewHierarchyAnimator { left: Int, top: Int, right: Int, - bottom: Int + bottom: Int, ): Boolean { return visibility != View.GONE && left != right && top != bottom } @@ -616,6 +619,7 @@ class ViewHierarchyAnimator { * not newly introduced margins are included. * * Base case + * * ``` * 1) origin=TOP * x---------x x---------x x---------x x---------x x---------x @@ -636,9 +640,11 @@ class ViewHierarchyAnimator { * x-----x x-------x | | * x---------x * ``` + * * In case the start and end values differ in the direction of the origin, and * [ignorePreviousValues] is false, the previous values are used and a translation is * included in addition to the view expansion. + * * ``` * origin=TOP_LEFT - (0,0,0,0) -> (30,30,70,70) * x @@ -660,7 +666,7 @@ class ViewHierarchyAnimator { previousTop: Int, previousRight: Int, previousBottom: Int, - ignorePreviousValues: Boolean + ignorePreviousValues: Boolean, ): Map<Bound, Int> { val startLeft = if (ignorePreviousValues) newLeft else previousLeft val startTop = if (ignorePreviousValues) newTop else previousTop @@ -727,7 +733,7 @@ class ViewHierarchyAnimator { Bound.LEFT to left, Bound.TOP to top, Bound.RIGHT to right, - Bound.BOTTOM to bottom + Bound.BOTTOM to bottom, ) } @@ -777,18 +783,17 @@ class ViewHierarchyAnimator { includeMargins: Boolean = false, ): Map<Bound, Int> { val marginAdjustment = - if (includeMargins && - (rootView.layoutParams is ViewGroup.MarginLayoutParams)) { + if (includeMargins && (rootView.layoutParams is ViewGroup.MarginLayoutParams)) { val marginLp = rootView.layoutParams as ViewGroup.MarginLayoutParams DimenHolder( left = marginLp.leftMargin, top = marginLp.topMargin, right = marginLp.rightMargin, - bottom = marginLp.bottomMargin + bottom = marginLp.bottomMargin, ) - } else { - DimenHolder(0, 0, 0, 0) - } + } else { + DimenHolder(0, 0, 0, 0) + } // These are the end values to use *if* this bound is part of the destination. val endLeft = left - marginAdjustment.left @@ -805,60 +810,69 @@ class ViewHierarchyAnimator { // - If destination=BOTTOM_LEFT, then endBottom == endTop AND endLeft == endRight. return when (destination) { - Hotspot.TOP -> mapOf( - Bound.TOP to endTop, - Bound.BOTTOM to endTop, - Bound.LEFT to left, - Bound.RIGHT to right, - ) - Hotspot.TOP_RIGHT -> mapOf( - Bound.TOP to endTop, - Bound.BOTTOM to endTop, - Bound.RIGHT to endRight, - Bound.LEFT to endRight, - ) - Hotspot.RIGHT -> mapOf( - Bound.RIGHT to endRight, - Bound.LEFT to endRight, - Bound.TOP to top, - Bound.BOTTOM to bottom, - ) - Hotspot.BOTTOM_RIGHT -> mapOf( - Bound.BOTTOM to endBottom, - Bound.TOP to endBottom, - Bound.RIGHT to endRight, - Bound.LEFT to endRight, - ) - Hotspot.BOTTOM -> mapOf( - Bound.BOTTOM to endBottom, - Bound.TOP to endBottom, - Bound.LEFT to left, - Bound.RIGHT to right, - ) - Hotspot.BOTTOM_LEFT -> mapOf( - Bound.BOTTOM to endBottom, - Bound.TOP to endBottom, - Bound.LEFT to endLeft, - Bound.RIGHT to endLeft, - ) - Hotspot.LEFT -> mapOf( - Bound.LEFT to endLeft, - Bound.RIGHT to endLeft, - Bound.TOP to top, - Bound.BOTTOM to bottom, - ) - Hotspot.TOP_LEFT -> mapOf( - Bound.TOP to endTop, - Bound.BOTTOM to endTop, - Bound.LEFT to endLeft, - Bound.RIGHT to endLeft, - ) - Hotspot.CENTER -> mapOf( - Bound.LEFT to (endLeft + endRight) / 2, - Bound.RIGHT to (endLeft + endRight) / 2, - Bound.TOP to (endTop + endBottom) / 2, - Bound.BOTTOM to (endTop + endBottom) / 2, - ) + Hotspot.TOP -> + mapOf( + Bound.TOP to endTop, + Bound.BOTTOM to endTop, + Bound.LEFT to left, + Bound.RIGHT to right, + ) + Hotspot.TOP_RIGHT -> + mapOf( + Bound.TOP to endTop, + Bound.BOTTOM to endTop, + Bound.RIGHT to endRight, + Bound.LEFT to endRight, + ) + Hotspot.RIGHT -> + mapOf( + Bound.RIGHT to endRight, + Bound.LEFT to endRight, + Bound.TOP to top, + Bound.BOTTOM to bottom, + ) + Hotspot.BOTTOM_RIGHT -> + mapOf( + Bound.BOTTOM to endBottom, + Bound.TOP to endBottom, + Bound.RIGHT to endRight, + Bound.LEFT to endRight, + ) + Hotspot.BOTTOM -> + mapOf( + Bound.BOTTOM to endBottom, + Bound.TOP to endBottom, + Bound.LEFT to left, + Bound.RIGHT to right, + ) + Hotspot.BOTTOM_LEFT -> + mapOf( + Bound.BOTTOM to endBottom, + Bound.TOP to endBottom, + Bound.LEFT to endLeft, + Bound.RIGHT to endLeft, + ) + Hotspot.LEFT -> + mapOf( + Bound.LEFT to endLeft, + Bound.RIGHT to endLeft, + Bound.TOP to top, + Bound.BOTTOM to bottom, + ) + Hotspot.TOP_LEFT -> + mapOf( + Bound.TOP to endTop, + Bound.BOTTOM to endTop, + Bound.LEFT to endLeft, + Bound.RIGHT to endLeft, + ) + Hotspot.CENTER -> + mapOf( + Bound.LEFT to (endLeft + endRight) / 2, + Bound.RIGHT to (endLeft + endRight) / 2, + Bound.TOP to (endTop + endBottom) / 2, + Bound.BOTTOM to (endTop + endBottom) / 2, + ) } } @@ -887,7 +901,7 @@ class ViewHierarchyAnimator { right: Int, bottom: Int, parentWidth: Int, - parentHeight: Int + parentHeight: Int, ): Map<Bound, Int> { val halfWidth = (right - left) / 2 val halfHeight = (bottom - top) / 2 @@ -945,7 +959,7 @@ class ViewHierarchyAnimator { Bound.LEFT to endLeft, Bound.TOP to endTop, Bound.RIGHT to endRight, - Bound.BOTTOM to endBottom + Bound.BOTTOM to endBottom, ) } @@ -954,7 +968,7 @@ class ViewHierarchyAnimator { listener: View.OnLayoutChangeListener, recursive: Boolean = false, animateChildren: Boolean = true, - excludedViews: Set<View> = emptySet() + excludedViews: Set<View> = emptySet(), ) { if (excludedViews.contains(view)) return @@ -973,7 +987,7 @@ class ViewHierarchyAnimator { listener, recursive = true, animateChildren = animateChildren, - excludedViews = excludedViews + excludedViews = excludedViews, ) } } @@ -1027,7 +1041,7 @@ class ViewHierarchyAnimator { PropertyValuesHolder.ofInt( PROPERTIES[bound], startValues.getValue(bound), - endValues.getValue(bound) + endValues.getValue(bound), ) ) } @@ -1056,9 +1070,10 @@ class ViewHierarchyAnimator { // listener. recursivelyRemoveListener(view) } - if (!cancelled) { - onAnimationEnd?.run() - } + // Run the end runnable regardless of whether the animation was cancelled or + // not - this ensures critical actions (like removing a window) always occur + // (see b/344049884). + onAnimationEnd?.run() } override fun onAnimationCancel(animation: Animator) { @@ -1077,17 +1092,19 @@ class ViewHierarchyAnimator { view: View, duration: Long, startDelay: Long, - interpolator: Interpolator + interpolator: Interpolator, ) { val animator = ObjectAnimator.ofFloat(view, "alpha", 1f) animator.startDelay = startDelay animator.duration = duration animator.interpolator = interpolator - animator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - view.setTag(R.id.tag_alpha_animator, null /* tag */) + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + view.setTag(R.id.tag_alpha_animator, null /* tag */) + } } - }) + ) (view.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.cancel() view.setTag(R.id.tag_alpha_animator, animator) @@ -1105,7 +1122,7 @@ class ViewHierarchyAnimator { RIGHT, BOTTOM_RIGHT, BOTTOM, - BOTTOM_LEFT + BOTTOM_LEFT, } private enum class Bound(val label: String, val overrideTag: Int) { @@ -1147,14 +1164,10 @@ class ViewHierarchyAnimator { }; abstract fun setValue(view: View, value: Int) + abstract fun getValue(view: View): Int } /** Simple data class to hold a set of dimens for left, top, right, bottom. */ - private data class DimenHolder( - val left: Int, - val top: Int, - val right: Int, - val bottom: Int, - ) + private data class DimenHolder(val left: Int, val top: Int, val right: Int, val bottom: Int) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt index d58e1bfbda83..eeab232542c0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryExt.kt @@ -288,7 +288,7 @@ private fun DragHandle(dialog: Dialog) { Modifier.padding(top = 16.dp, bottom = 6.dp) .semantics { contentDescription = dragHandleContentDescription } .clickable { dialog.dismiss() }, - color = MaterialTheme.colorScheme.outlineVariant, + color = MaterialTheme.colorScheme.onSurfaceVariant, shape = MaterialTheme.shapes.extraLarge, ) { Box(Modifier.size(width = 32.dp, height = 4.dp)) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt index ae18aac66215..052e60e7ac9a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepository.kt @@ -25,6 +25,9 @@ interface SecureSettingsRepository { /** Returns a [Flow] tracking the value of a setting as an [Int]. */ fun intSetting(name: String, defaultValue: Int = 0): Flow<Int> + /** Returns a [Flow] tracking the value of a setting as a [Boolean]. */ + fun boolSetting(name: String, defaultValue: Boolean = false): Flow<Boolean> + /** Updates the value of the setting with the given name. */ suspend fun setInt(name: String, value: Int) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryImpl.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryImpl.kt index 8b9fcb496f59..9f37959663d7 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryImpl.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryImpl.kt @@ -63,6 +63,10 @@ class SecureSettingsRepositoryImpl( .flowOn(backgroundDispatcher) } + override fun boolSetting(name: String, defaultValue: Boolean): Flow<Boolean> { + return intSetting(name, if (defaultValue) 1 else 0).map { it != 0 } + } + override suspend fun setInt(name: String, value: Int) { withContext(backgroundDispatcher) { Settings.Secure.putInt(contentResolver, name, value) } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt index 8cda9b3d9985..b5f991cdb9b4 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepository.kt @@ -25,6 +25,9 @@ interface SystemSettingsRepository { /** Returns a [Flow] tracking the value of a setting as an [Int]. */ fun intSetting(name: String, defaultValue: Int = 0): Flow<Int> + /** Returns a [Flow] tracking the value of a setting as a [Boolean]. */ + fun boolSetting(name: String, defaultValue: Boolean = false): Flow<Boolean> + /** Updates the value of the setting with the given name. */ suspend fun setInt(name: String, value: Int) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryImpl.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryImpl.kt index b039a320b987..8485d4db2d20 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryImpl.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/settings/data/repository/SystemSettingsRepositoryImpl.kt @@ -63,6 +63,10 @@ class SystemSettingsRepositoryImpl( .flowOn(backgroundDispatcher) } + override fun boolSetting(name: String, defaultValue: Boolean): Flow<Boolean> { + return intSetting(name, if (defaultValue) 1 else 0).map { it != 0 } + } + override suspend fun setInt(name: String, value: Int) { withContext(backgroundDispatcher) { Settings.System.putInt(contentResolver, name, value) } } diff --git a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt index 37b97926afa4..21d8d4648824 100644 --- a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt +++ b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSecureSettingsRepository.kt @@ -28,6 +28,10 @@ class FakeSecureSettingsRepository : SecureSettingsRepository { return settings.map { it.getOrDefault(name, defaultValue.toString()) }.map { it.toInt() } } + override fun boolSetting(name: String, defaultValue: Boolean): Flow<Boolean> { + return intSetting(name, if (defaultValue) 1 else 0).map { it != 0 } + } + override suspend fun setInt(name: String, value: Int) { settings.value = settings.value.toMutableMap().apply { this[name] = value.toString() } } diff --git a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt index 7da2b40fd57a..f6c053f5b1dc 100644 --- a/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt +++ b/packages/SystemUI/customization/tests/utils/src/com/android/systemui/shared/settings/data/repository/FakeSystemSettingsRepository.kt @@ -28,6 +28,10 @@ class FakeSystemSettingsRepository : SystemSettingsRepository { return settings.map { it.getOrDefault(name, defaultValue.toString()) }.map { it.toInt() } } + override fun boolSetting(name: String, defaultValue: Boolean): Flow<Boolean> { + return intSetting(name, if (defaultValue) 1 else 0).map { it != 0 } + } + override suspend fun setInt(name: String, value: Int) { settings.value = settings.value.toMutableMap().apply { this[name] = value.toString() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt index a8c3af9488f0..8db82d58ecc5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt @@ -24,8 +24,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper -class -ViewHierarchyAnimatorTest : SysuiTestCase() { +class ViewHierarchyAnimatorTest : SysuiTestCase() { companion object { private const val TEST_DURATION = 1000L private val TEST_INTERPOLATOR = Interpolators.LINEAR @@ -49,9 +48,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */) // animate() - var success = ViewHierarchyAnimator.animate( - rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION - ) + var success = + ViewHierarchyAnimator.animate( + rootView, + interpolator = TEST_INTERPOLATOR, + duration = TEST_DURATION, + ) rootView.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -64,9 +66,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { ViewHierarchyAnimator.stopAnimating(rootView) // animateNextUpdate() - success = ViewHierarchyAnimator.animateNextUpdate( - rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION - ) + success = + ViewHierarchyAnimator.animateNextUpdate( + rootView, + interpolator = TEST_INTERPOLATOR, + duration = TEST_DURATION, + ) rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */) assertTrue(success) @@ -79,9 +84,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // animateAddition() rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + interpolator = TEST_INTERPOLATOR, + duration = TEST_DURATION, + ) rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */) assertTrue(success) @@ -93,9 +101,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // animateRemoval() setUpRootWithChildren() val child = rootView.getChildAt(0) - success = ViewHierarchyAnimator.animateRemoval( - child, interpolator = TEST_INTERPOLATOR, duration = TEST_DURATION - ) + success = + ViewHierarchyAnimator.animateRemoval( + child, + interpolator = TEST_INTERPOLATOR, + duration = TEST_DURATION, + ) assertTrue(success) assertNotNull(child.getTag(R.id.tag_animator)) @@ -185,7 +196,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // Change all bounds. rootView.measure( View.MeasureSpec.makeMeasureSpec(190, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), ) rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */) @@ -211,14 +222,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { fun animatesRootAndChildren_withExcludedViews() { setUpRootWithChildren() - val success = ViewHierarchyAnimator.animate( - rootView, - excludedViews = setOf(rootView.getChildAt(0)) - ) + val success = + ViewHierarchyAnimator.animate(rootView, excludedViews = setOf(rootView.getChildAt(0))) // Change all bounds. rootView.measure( - View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), ) rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */) @@ -245,14 +254,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { fun animatesRootOnly() { setUpRootWithChildren() - val success = ViewHierarchyAnimator.animate( - rootView, - animateChildren = false - ) + val success = ViewHierarchyAnimator.animate(rootView, animateChildren = false) // Change all bounds. rootView.measure( - View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), ) rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */) @@ -351,10 +357,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { fun animatesAppearingViewsRespectingOrigin() { // CENTER. rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */) - var success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.CENTER - ) + var success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.CENTER, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -364,10 +371,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // LEFT. rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.LEFT - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.LEFT, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -377,10 +385,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // TOP_LEFT. rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -390,10 +399,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // TOP. rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.TOP - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.TOP, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -403,10 +413,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // TOP_RIGHT. rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -416,10 +427,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // RIGHT. rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.RIGHT - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.RIGHT, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -429,10 +441,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // BOTTOM_RIGHT. rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -442,10 +455,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // BOTTOM. rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.BOTTOM - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.BOTTOM, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -455,10 +469,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // BOTTOM_LEFT. rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -471,11 +486,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { fun animatesAppearingViewsRespectingMargins() { // CENTER. rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */) - var success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.CENTER, - includeMargins = true - ) + var success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.CENTER, + includeMargins = true, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -485,10 +501,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // LEFT. rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, origin = ViewHierarchyAnimator.Hotspot.LEFT, - includeMargins = true - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.LEFT, + includeMargins = true, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -498,11 +516,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // TOP_LEFT. rootView.layout(0 /* l */, 0 /* t */, 0 /* r */, 0 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT, - includeMargins = true - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.TOP_LEFT, + includeMargins = true, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -512,10 +531,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // TOP. rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, origin = ViewHierarchyAnimator.Hotspot.TOP, - includeMargins = true - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.TOP, + includeMargins = true, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -525,11 +546,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // TOP_RIGHT. rootView.layout(150 /* l */, 0 /* t */, 150 /* r */, 0 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT, - includeMargins = true - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.TOP_RIGHT, + includeMargins = true, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -539,11 +561,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // RIGHT. rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.RIGHT, - includeMargins = true - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.RIGHT, + includeMargins = true, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -553,11 +576,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // BOTTOM_RIGHT. rootView.layout(150 /* l */, 150 /* t */, 150 /* r */, 150 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT, - includeMargins = true - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT, + includeMargins = true, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -567,11 +591,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // BOTTOM. rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.BOTTOM, - includeMargins = true - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.BOTTOM, + includeMargins = true, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -581,11 +606,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // BOTTOM_LEFT. rootView.layout(0 /* l */, 150 /* t */, 0 /* r */, 150 /* b */) - success = ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT, - includeMargins = true - ) + success = + ViewHierarchyAnimator.animateAddition( + rootView, + origin = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT, + includeMargins = true, + ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) assertTrue(success) @@ -626,7 +652,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { ViewHierarchyAnimator.animateAddition( rootView, includeFadeIn = true, - fadeInInterpolator = Interpolators.LINEAR + fadeInInterpolator = Interpolators.LINEAR, ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) @@ -641,7 +667,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { ViewHierarchyAnimator.animateAddition( rootView, includeFadeIn = true, - fadeInInterpolator = Interpolators.LINEAR + fadeInInterpolator = Interpolators.LINEAR, ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) @@ -663,7 +689,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { ViewHierarchyAnimator.animateAddition( rootView, includeFadeIn = true, - fadeInInterpolator = Interpolators.LINEAR + fadeInInterpolator = Interpolators.LINEAR, ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) @@ -680,7 +706,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { ViewHierarchyAnimator.animateAddition( rootView, includeFadeIn = true, - fadeInInterpolator = Interpolators.LINEAR + fadeInInterpolator = Interpolators.LINEAR, ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) @@ -692,7 +718,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { ViewHierarchyAnimator.animateAddition( rootView, includeFadeIn = true, - fadeInInterpolator = Interpolators.LINEAR + fadeInInterpolator = Interpolators.LINEAR, ) // THEN the alpha remains at its current value (it doesn't get reset to 0) @@ -721,7 +747,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { ViewHierarchyAnimator.animateAddition( rootView, includeFadeIn = false, - fadeInInterpolator = Interpolators.LINEAR + fadeInInterpolator = Interpolators.LINEAR, ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) @@ -738,10 +764,10 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val onAnimationEndRunnable = { runnableRun = true } ViewHierarchyAnimator.animateAddition( - rootView, - origin = ViewHierarchyAnimator.Hotspot.CENTER, - includeMargins = true, - onAnimationEnd = onAnimationEndRunnable + rootView, + origin = ViewHierarchyAnimator.Hotspot.CENTER, + includeMargins = true, + onAnimationEnd = onAnimationEndRunnable, ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) @@ -751,7 +777,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } @Test - fun animateAddition_runnableDoesNotRunWhenAnimationCancelled() { + fun animateAddition_runnableRunsWhenAnimationCancelled() { var runnableRun = false val onAnimationEndRunnable = { runnableRun = true } @@ -759,13 +785,13 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { rootView, origin = ViewHierarchyAnimator.Hotspot.CENTER, includeMargins = true, - onAnimationEnd = onAnimationEndRunnable + onAnimationEnd = onAnimationEndRunnable, ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) cancelAnimation(rootView) - assertEquals(false, runnableRun) + assertEquals(true, runnableRun) } @Test @@ -777,7 +803,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { rootView, origin = ViewHierarchyAnimator.Hotspot.CENTER, includeMargins = true, - onAnimationEnd = onAnimationEndRunnable + onAnimationEnd = onAnimationEndRunnable, ) rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */) @@ -791,11 +817,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { setUpRootWithChildren() val child = rootView.getChildAt(0) - val success = ViewHierarchyAnimator.animateRemoval( - child, - destination = ViewHierarchyAnimator.Hotspot.LEFT, - interpolator = Interpolators.LINEAR - ) + val success = + ViewHierarchyAnimator.animateRemoval( + child, + destination = ViewHierarchyAnimator.Hotspot.LEFT, + interpolator = Interpolators.LINEAR, + ) assertTrue(success) assertNotNull(child.getTag(R.id.tag_animator)) @@ -820,11 +847,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { rootView.addView(onlyChild) forceLayout() - val success = ViewHierarchyAnimator.animateRemoval( - onlyChild, - destination = ViewHierarchyAnimator.Hotspot.LEFT, - interpolator = Interpolators.LINEAR - ) + val success = + ViewHierarchyAnimator.animateRemoval( + onlyChild, + destination = ViewHierarchyAnimator.Hotspot.LEFT, + interpolator = Interpolators.LINEAR, + ) assertTrue(success) assertNotNull(onlyChild.getTag(R.id.tag_animator)) @@ -845,9 +873,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { setUpRootWithChildren() var removedChild = rootView.getChildAt(0) var remainingChild = rootView.getChildAt(1) - var success = ViewHierarchyAnimator.animateRemoval( - removedChild, destination = ViewHierarchyAnimator.Hotspot.CENTER - ) + var success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.CENTER, + ) // Ensure that the layout happens before the checks. forceLayout() @@ -863,9 +893,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { setUpRootWithChildren() removedChild = rootView.getChildAt(0) remainingChild = rootView.getChildAt(1) - success = ViewHierarchyAnimator.animateRemoval( - removedChild, destination = ViewHierarchyAnimator.Hotspot.LEFT - ) + success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.LEFT, + ) // Ensure that the layout happens before the checks. forceLayout() @@ -881,9 +913,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { setUpRootWithChildren() removedChild = rootView.getChildAt(0) remainingChild = rootView.getChildAt(1) - success = ViewHierarchyAnimator.animateRemoval( - removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT - ) + success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT, + ) // Ensure that the layout happens before the checks. forceLayout() @@ -899,9 +933,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { setUpRootWithChildren() removedChild = rootView.getChildAt(0) remainingChild = rootView.getChildAt(1) - success = ViewHierarchyAnimator.animateRemoval( - removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP - ) + success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.TOP, + ) // Ensure that the layout happens before the checks. forceLayout() @@ -917,9 +953,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { setUpRootWithChildren() removedChild = rootView.getChildAt(0) remainingChild = rootView.getChildAt(1) - success = ViewHierarchyAnimator.animateRemoval( - removedChild, destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT - ) + success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT, + ) // Ensure that the layout happens before the checks. forceLayout() @@ -935,9 +973,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { setUpRootWithChildren() removedChild = rootView.getChildAt(0) remainingChild = rootView.getChildAt(1) - success = ViewHierarchyAnimator.animateRemoval( - removedChild, destination = ViewHierarchyAnimator.Hotspot.RIGHT - ) + success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.RIGHT, + ) // Ensure that the layout happens before the checks. forceLayout() @@ -953,9 +993,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { setUpRootWithChildren() removedChild = rootView.getChildAt(0) remainingChild = rootView.getChildAt(1) - success = ViewHierarchyAnimator.animateRemoval( - removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT - ) + success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT, + ) // Ensure that the layout happens before the checks. forceLayout() @@ -971,9 +1013,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { setUpRootWithChildren() removedChild = rootView.getChildAt(0) remainingChild = rootView.getChildAt(1) - success = ViewHierarchyAnimator.animateRemoval( - removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM - ) + success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.BOTTOM, + ) // Ensure that the layout happens before the checks. forceLayout() @@ -989,9 +1033,11 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { setUpRootWithChildren() removedChild = rootView.getChildAt(0) remainingChild = rootView.getChildAt(1) - success = ViewHierarchyAnimator.animateRemoval( - removedChild, destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT - ) + success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT, + ) // Ensure that the layout happens before the checks. forceLayout() @@ -1014,11 +1060,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val originalRight = removedChild.right val originalBottom = removedChild.bottom - val success = ViewHierarchyAnimator.animateRemoval( - removedChild, - destination = ViewHierarchyAnimator.Hotspot.CENTER, - includeMargins = true, - ) + val success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.CENTER, + includeMargins = true, + ) forceLayout() assertTrue(success) @@ -1027,13 +1074,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val expectedX = ((originalLeft - M_LEFT) + (originalRight + M_RIGHT)) / 2 val expectedY = ((originalTop - M_TOP) + (originalBottom + M_BOTTOM)) / 2 - checkBounds( - removedChild, - l = expectedX, - t = expectedY, - r = expectedX, - b = expectedY - ) + checkBounds(removedChild, l = expectedX, t = expectedY, r = expectedX, b = expectedY) } @Test @@ -1044,11 +1085,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val originalTop = removedChild.top val originalBottom = removedChild.bottom - val success = ViewHierarchyAnimator.animateRemoval( - removedChild, - destination = ViewHierarchyAnimator.Hotspot.LEFT, - includeMargins = true, - ) + val success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.LEFT, + includeMargins = true, + ) forceLayout() assertTrue(success) @@ -1059,7 +1101,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { l = originalLeft - M_LEFT, t = originalTop, r = originalLeft - M_LEFT, - b = originalBottom + b = originalBottom, ) } @@ -1070,11 +1112,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val originalLeft = removedChild.left val originalTop = removedChild.top - val success = ViewHierarchyAnimator.animateRemoval( - removedChild, - destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT, - includeMargins = true, - ) + val success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.TOP_LEFT, + includeMargins = true, + ) forceLayout() assertTrue(success) @@ -1085,7 +1128,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { l = originalLeft - M_LEFT, t = originalTop - M_TOP, r = originalLeft - M_LEFT, - b = originalTop - M_TOP + b = originalTop - M_TOP, ) } @@ -1097,11 +1140,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val originalTop = removedChild.top val originalRight = removedChild.right - val success = ViewHierarchyAnimator.animateRemoval( - removedChild, - destination = ViewHierarchyAnimator.Hotspot.TOP, - includeMargins = true, - ) + val success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.TOP, + includeMargins = true, + ) forceLayout() assertTrue(success) @@ -1112,7 +1156,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { l = originalLeft, t = originalTop - M_TOP, r = originalRight, - b = originalTop - M_TOP + b = originalTop - M_TOP, ) } @@ -1123,11 +1167,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val originalTop = removedChild.top val originalRight = removedChild.right - val success = ViewHierarchyAnimator.animateRemoval( - removedChild, - destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT, - includeMargins = true, - ) + val success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.TOP_RIGHT, + includeMargins = true, + ) forceLayout() assertTrue(success) @@ -1138,7 +1183,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { l = originalRight + M_RIGHT, t = originalTop - M_TOP, r = originalRight + M_RIGHT, - b = originalTop - M_TOP + b = originalTop - M_TOP, ) } @@ -1150,11 +1195,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val originalRight = removedChild.right val originalBottom = removedChild.bottom - val success = ViewHierarchyAnimator.animateRemoval( - removedChild, - destination = ViewHierarchyAnimator.Hotspot.RIGHT, - includeMargins = true, - ) + val success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.RIGHT, + includeMargins = true, + ) forceLayout() assertTrue(success) @@ -1165,7 +1211,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { l = originalRight + M_RIGHT, t = originalTop, r = originalRight + M_RIGHT, - b = originalBottom + b = originalBottom, ) } @@ -1176,11 +1222,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val originalRight = removedChild.right val originalBottom = removedChild.bottom - val success = ViewHierarchyAnimator.animateRemoval( - removedChild, - destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT, - includeMargins = true, - ) + val success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.BOTTOM_RIGHT, + includeMargins = true, + ) forceLayout() assertTrue(success) @@ -1191,7 +1238,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { l = originalRight + M_RIGHT, t = originalBottom + M_BOTTOM, r = originalRight + M_RIGHT, - b = originalBottom + M_BOTTOM + b = originalBottom + M_BOTTOM, ) } @@ -1203,11 +1250,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val originalRight = removedChild.right val originalBottom = removedChild.bottom - val success = ViewHierarchyAnimator.animateRemoval( - removedChild, - destination = ViewHierarchyAnimator.Hotspot.BOTTOM, - includeMargins = true, - ) + val success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.BOTTOM, + includeMargins = true, + ) forceLayout() assertTrue(success) @@ -1218,7 +1266,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { l = originalLeft, t = originalBottom + M_BOTTOM, r = originalRight, - b = originalBottom + M_BOTTOM + b = originalBottom + M_BOTTOM, ) } @@ -1229,11 +1277,12 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val originalLeft = removedChild.left val originalBottom = removedChild.bottom - val success = ViewHierarchyAnimator.animateRemoval( - removedChild, - destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT, - includeMargins = true, - ) + val success = + ViewHierarchyAnimator.animateRemoval( + removedChild, + destination = ViewHierarchyAnimator.Hotspot.BOTTOM_LEFT, + includeMargins = true, + ) forceLayout() assertTrue(success) @@ -1244,9 +1293,10 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { l = originalLeft - M_LEFT, t = originalBottom + M_BOTTOM, r = originalLeft - M_LEFT, - b = originalBottom + M_BOTTOM + b = originalBottom + M_BOTTOM, ) } + /* ******** end of animatesViewRemoval_includeMarginsTrue tests ******** */ @Test @@ -1256,9 +1306,8 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val child = rootView.getChildAt(0) as ViewGroup val firstGrandChild = child.getChildAt(0) val secondGrandChild = child.getChildAt(1) - val success = ViewHierarchyAnimator.animateRemoval( - child, interpolator = Interpolators.LINEAR - ) + val success = + ViewHierarchyAnimator.animateRemoval(child, interpolator = Interpolators.LINEAR) assertTrue(success) assertNotNull(child.getTag(R.id.tag_animator)) @@ -1288,9 +1337,8 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val removedChild = rootView.getChildAt(0) val remainingChild = rootView.getChildAt(1) - val success = ViewHierarchyAnimator.animateRemoval( - removedChild, interpolator = Interpolators.LINEAR - ) + val success = + ViewHierarchyAnimator.animateRemoval(removedChild, interpolator = Interpolators.LINEAR) // Ensure that the layout happens before the checks. forceLayout() @@ -1315,17 +1363,14 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { forceLayout() val removedView = rootView.getChildAt(0) - ViewHierarchyAnimator.animateRemoval( - removedView, - onAnimationEnd = onAnimationEndRunnable - ) + ViewHierarchyAnimator.animateRemoval(removedView, onAnimationEnd = onAnimationEndRunnable) endAnimation(removedView) assertEquals(true, runnableRun) } @Test - fun animateRemoval_runnableDoesNotRunWhenAnimationCancelled() { + fun animateRemoval_runnableRunsWhenAnimationCancelled() { var runnableRun = false val onAnimationEndRunnable = { runnableRun = true } @@ -1333,13 +1378,10 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { forceLayout() val removedView = rootView.getChildAt(0) - ViewHierarchyAnimator.animateRemoval( - removedView, - onAnimationEnd = onAnimationEndRunnable - ) + ViewHierarchyAnimator.animateRemoval(removedView, onAnimationEnd = onAnimationEndRunnable) cancelAnimation(removedView) - assertEquals(false, runnableRun) + assertEquals(true, runnableRun) } @Test @@ -1351,10 +1393,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { forceLayout() val removedView = rootView.getChildAt(0) - ViewHierarchyAnimator.animateRemoval( - removedView, - onAnimationEnd = onAnimationEndRunnable - ) + ViewHierarchyAnimator.animateRemoval(removedView, onAnimationEnd = onAnimationEndRunnable) advanceAnimation(removedView, 0.5f) assertEquals(false, runnableRun) @@ -1370,7 +1409,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { rootView.addView(secondChild) rootView.measure( View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), ) rootView.layout(0 /* l */, 0 /* t */, 100 /* r */, 100 /* b */) @@ -1378,7 +1417,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { // Change all bounds. rootView.measure( View.MeasureSpec.makeMeasureSpec(150, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), ) rootView.layout(0 /* l */, 0 /* t */, 150 /* r */, 100 /* b */) @@ -1501,7 +1540,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { checkBounds(rootView, l = 0, t = 15, r = 70, b = 80) // Change all bounds again. - rootView.layout(10 /* l */, 10 /* t */, 50/* r */, 50 /* b */) + rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */) assertNull(rootView.getTag(R.id.tag_animator)) checkBounds(rootView, l = 10, t = 10, r = 50, b = 50) @@ -1523,7 +1562,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { ViewHierarchyAnimator.stopAnimating(rootView) // Change all bounds again. - rootView.layout(10 /* l */, 10 /* t */, 50/* r */, 50 /* b */) + rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */) assertNull(rootView.getTag(R.id.tag_animator)) checkBounds(rootView, l = 10, t = 10, r = 50, b = 50) @@ -1543,10 +1582,8 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { val secondChild = View(mContext) rootView.addView(secondChild) - val firstChildParams = LinearLayout.LayoutParams( - 0 /* width */, - LinearLayout.LayoutParams.MATCH_PARENT - ) + val firstChildParams = + LinearLayout.LayoutParams(0 /* width */, LinearLayout.LayoutParams.MATCH_PARENT) firstChildParams.weight = 0.5f if (includeMarginsOnFirstChild) { firstChildParams.leftMargin = M_LEFT @@ -1556,23 +1593,25 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } firstChild.layoutParams = firstChildParams - val secondChildParams = LinearLayout.LayoutParams( - 0 /* width */, - LinearLayout.LayoutParams.MATCH_PARENT - ) + val secondChildParams = + LinearLayout.LayoutParams(0 /* width */, LinearLayout.LayoutParams.MATCH_PARENT) secondChildParams.weight = 0.5f secondChild.layoutParams = secondChildParams firstGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */) - (firstGrandChild.layoutParams as RelativeLayout.LayoutParams) - .addRule(RelativeLayout.ALIGN_PARENT_START) - (firstGrandChild.layoutParams as RelativeLayout.LayoutParams) - .addRule(RelativeLayout.ALIGN_PARENT_TOP) + (firstGrandChild.layoutParams as RelativeLayout.LayoutParams).addRule( + RelativeLayout.ALIGN_PARENT_START + ) + (firstGrandChild.layoutParams as RelativeLayout.LayoutParams).addRule( + RelativeLayout.ALIGN_PARENT_TOP + ) secondGrandChild.layoutParams = RelativeLayout.LayoutParams(40 /* width */, 40 /* height */) - (secondGrandChild.layoutParams as RelativeLayout.LayoutParams) - .addRule(RelativeLayout.ALIGN_PARENT_END) - (secondGrandChild.layoutParams as RelativeLayout.LayoutParams) - .addRule(RelativeLayout.ALIGN_PARENT_BOTTOM) + (secondGrandChild.layoutParams as RelativeLayout.LayoutParams).addRule( + RelativeLayout.ALIGN_PARENT_END + ) + (secondGrandChild.layoutParams as RelativeLayout.LayoutParams).addRule( + RelativeLayout.ALIGN_PARENT_BOTTOM + ) forceLayout() } @@ -1580,7 +1619,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { private fun forceLayout() { rootView.measure( View.MeasureSpec.makeMeasureSpec(200 /* width */, View.MeasureSpec.AT_MOST), - View.MeasureSpec.makeMeasureSpec(100 /* height */, View.MeasureSpec.AT_MOST) + View.MeasureSpec.makeMeasureSpec(100 /* height */, View.MeasureSpec.AT_MOST), ) rootView.layout(0 /* l */, 0 /* t */, 200 /* r */, 100 /* b */) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt index 116b7054ed5d..7cdfb0eb2451 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.coroutines.collectLastValue import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory +import com.android.systemui.kosmos.brightnessWarningToast import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.res.R @@ -61,6 +62,7 @@ class BrightnessSliderViewModelTest : SysuiTestCase() { sliderHapticsViewModelFactory, brightnessMirrorShowingInteractor, supportsMirroring = true, + brightnessWarningToast, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java index 8ccaf6bc0651..0f631509bfba 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerControllerTest.java @@ -300,7 +300,7 @@ public class QRCodeScannerControllerTest extends SysuiTestCase { mSecureSettings.putStringForUser(LOCK_SCREEN_SHOW_QR_CODE_SCANNER, "0", UserHandle.USER_CURRENT); verifyActivityDetails("abc/.def"); - assertThat(mController.isEnabledForLockScreenButton()).isFalse(); + assertThat(mController.isEnabledForLockScreenButton()).isTrue(); assertThat(mController.isAllowedOnLockScreen()).isTrue(); assertThat(mController.isAbleToLaunchScannerActivity()).isTrue(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt index 5cba325450e6..03feceb7c15a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt @@ -318,6 +318,63 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() } } + @Test + fun qqsMediaExpansion_collapsedMediaInLandscape() = + with(kosmos) { + testScope.testWithinLifecycle { + setCollapsedMediaInLandscape(true) + setMediaState(ACTIVE_MEDIA) + + setConfigurationForMediaInRow(mediaInRow = false) + Snapshot.sendApplyNotifications() + runCurrent() + assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED) + + setConfigurationForMediaInRow(mediaInRow = true) + Snapshot.sendApplyNotifications() + runCurrent() + assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.COLLAPSED) + } + } + + @Test + fun qqsMediaExpansion_notCollapsedMediaInLandscape_alwaysExpanded() = + with(kosmos) { + testScope.testWithinLifecycle { + setCollapsedMediaInLandscape(false) + setMediaState(ACTIVE_MEDIA) + + setConfigurationForMediaInRow(mediaInRow = false) + Snapshot.sendApplyNotifications() + runCurrent() + assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED) + + setConfigurationForMediaInRow(mediaInRow = true) + Snapshot.sendApplyNotifications() + runCurrent() + assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED) + } + } + + @Test + fun qqsMediaExpansion_reactsToChangesInCollapsedMediaInLandscape() = + with(kosmos) { + testScope.testWithinLifecycle { + setConfigurationForMediaInRow(mediaInRow = true) + setMediaState(ACTIVE_MEDIA) + + setCollapsedMediaInLandscape(false) + Snapshot.sendApplyNotifications() + runCurrent() + assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED) + + setCollapsedMediaInLandscape(true) + Snapshot.sendApplyNotifications() + runCurrent() + assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.COLLAPSED) + } + } + private fun TestScope.setMediaState(state: MediaState) { with(kosmos) { val activeMedia = state == ACTIVE_MEDIA @@ -331,6 +388,14 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() runCurrent() } + private fun TestScope.setCollapsedMediaInLandscape(collapsed: Boolean) { + with(kosmos) { + overrideResource(R.bool.config_quickSettingsMediaLandscapeCollapsed, collapsed) + fakeConfigurationRepository.onAnyConfigurationChange() + } + runCurrent() + } + companion object { private const val QS_DISABLE_FLAG = StatusBarManager.DISABLE2_QUICK_SETTINGS diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt index 41e2467f798b..ae7719bca2a8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt @@ -16,13 +16,19 @@ package com.android.systemui.settings.brightness +import android.hardware.display.BrightnessInfo import android.hardware.display.DisplayManager import android.os.Handler +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.service.vr.IVrManager import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper +import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.log.LogBuffer import com.android.systemui.settings.DisplayTracker @@ -30,13 +36,16 @@ import com.android.systemui.settings.UserTracker import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -44,7 +53,8 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) @RunWithLooper class BrightnessControllerTest : SysuiTestCase() { - + @get:Rule + public val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() private val executor = FakeExecutor(FakeSystemClock()) private val secureSettings = FakeSettings() @Mock private lateinit var toggleSlider: ToggleSlider @@ -53,6 +63,7 @@ class BrightnessControllerTest : SysuiTestCase() { @Mock private lateinit var displayManager: DisplayManager @Mock private lateinit var iVrManager: IVrManager @Mock private lateinit var logger: LogBuffer + @Mock private lateinit var display: Display private lateinit var testableLooper: TestableLooper @@ -63,9 +74,11 @@ class BrightnessControllerTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) + val contextSpy = spy(context) + whenever(contextSpy.getDisplay()).thenReturn(display) underTest = BrightnessController( - context, + contextSpy, toggleSlider, userTracker, displayTracker, @@ -105,4 +118,21 @@ class BrightnessControllerTest : SysuiTestCase() { assertThat(messagesProcessed).isEqualTo(1) } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_SHOW_TOAST_WHEN_APP_CONTROL_BRIGHTNESS) + fun testOnChange_showToastWhenAppOverridesBrightness() { + val brightnessInfo = BrightnessInfo( + 0.45f, 0.45f, 0.0f, 1.0f, BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, + 1.0f /* highBrightnessTransitionPoint */, + BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, + true /* isBrightnessOverrideByWindow */ + ) + whenever(display.brightnessInfo).thenReturn(brightnessInfo) + underTest.registerCallbacks() + testableLooper.processAllMessages() + + underTest.onChanged(true /* tracking */, 100 /* value */, false /* stopTracking */) + verify(toggleSlider).showToast(any()) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt index 637a12c11690..3697c3151333 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/settings/brightness/BrightnessSliderControllerTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.haptics.slider.HapticSlider import com.android.systemui.haptics.slider.HapticSliderPlugin import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.settings.brightness.ui.BrightnessWarningToast import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.BrightnessMirrorController import com.android.systemui.util.mockito.any @@ -64,6 +65,7 @@ class BrightnessSliderControllerTest : SysuiTestCase() { @Mock private lateinit var vibratorHelper: VibratorHelper @Mock private lateinit var msdlPlayer: MSDLPlayer @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var brightnessWarningToast: BrightnessWarningToast @Captor private lateinit var seekBarChangeCaptor: ArgumentCaptor<SeekBar.OnSeekBarChangeListener> @@ -94,6 +96,7 @@ class BrightnessSliderControllerTest : SysuiTestCase() { HapticSlider.SeekBar(seekBar), ), activityStarter, + brightnessWarningToast, ) mController.init() mController.setOnChangedListener(listener) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java index 44782529e5c3..33a08035a7b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/KeyguardIndicationControllerBaseTest.java @@ -86,6 +86,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.KeyguardIndicationTextView; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.user.domain.interactor.UserLogoutInteractor; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.util.wakelock.WakeLockFake; @@ -160,6 +161,8 @@ public class KeyguardIndicationControllerBaseTest extends SysuiTestCase { @Mock protected DeviceEntryFingerprintAuthInteractor mDeviceEntryFingerprintAuthInteractor; @Mock + protected UserLogoutInteractor mUserLogoutInteractor; + @Mock protected ScreenLifecycle mScreenLifecycle; @Mock protected AuthController mAuthController; @@ -248,6 +251,9 @@ public class KeyguardIndicationControllerBaseTest extends SysuiTestCase { when(mFaceHelpMessageDeferralFactory.create()).thenReturn(mFaceHelpMessageDeferral); when(mDeviceEntryFingerprintAuthInteractor.isEngaged()).thenReturn(mock(StateFlow.class)); + StateFlow mockLogoutEnabledFlow = mock(StateFlow.class); + when(mockLogoutEnabledFlow.getValue()).thenReturn(false); + when(mUserLogoutInteractor.isLogoutEnabled()).thenReturn(mockLogoutEnabledFlow); mIndicationHelper = new IndicationHelper(mKeyguardUpdateMonitor); @@ -291,7 +297,8 @@ public class KeyguardIndicationControllerBaseTest extends SysuiTestCase { KeyguardInteractorFactory.create(mFlags).getKeyguardInteractor(), mBiometricMessageInteractor, mDeviceEntryFingerprintAuthInteractor, - mDeviceEntryFaceAuthInteractor + mDeviceEntryFaceAuthInteractor, + mUserLogoutInteractor ); mController.init(); mController.setIndicationArea(mIndicationArea); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt index 0efd591940f2..11a125a21be0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt @@ -16,29 +16,35 @@ package com.android.systemui.statusbar.chips.screenrecord.domain.interactor +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask import com.android.systemui.screenrecord.data.model.ScreenRecordModel import com.android.systemui.screenrecord.data.repository.screenRecordRepository import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) class ScreenRecordChipInteractorTest : SysuiTestCase() { - private val kosmos = Kosmos().also { it.testCase = this } + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val screenRecordRepo = kosmos.screenRecordRepository private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository @@ -116,6 +122,137 @@ class ScreenRecordChipInteractorTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) + fun screenRecordState_flagOff_doesNotAutomaticallySwitchToRecordingBasedOnTime() = + testScope.runTest { + val latest by collectLastValue(underTest.screenRecordState) + + // WHEN screen record should start in 900ms + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) + assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) + + // WHEN 900ms has elapsed + advanceTimeBy(901) + + // THEN we don't automatically update to the recording state if the flag is off + assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) + fun screenRecordState_flagOn_automaticallySwitchesToRecordingBasedOnTime() = + testScope.runTest { + val latest by collectLastValue(underTest.screenRecordState) + + // WHEN screen record should start in 900ms + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) + assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) + + // WHEN 900ms has elapsed + advanceTimeBy(901) + + // THEN we automatically update to the recording state + assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null)) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) + fun screenRecordState_recordingBeginsEarly_switchesToRecording() = + testScope.runTest { + val latest by collectLastValue(underTest.screenRecordState) + + // WHEN screen record should start in 900ms + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) + assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(900)) + + // WHEN we update to the Recording state earlier than 900ms + advanceTimeBy(800) + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording + val task = createTask(taskId = 1) + mediaProjectionRepo.mediaProjectionState.value = + MediaProjectionState.Projecting.SingleTask( + "host.package", + hostDeviceName = null, + task, + ) + + // THEN we immediately switch to Recording, and we have the task + assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) + + // WHEN more than 900ms has elapsed + advanceTimeBy(200) + + // THEN we still stay in the Recording state and we have the task + assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = task)) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) + fun screenRecordState_secondRecording_doesNotAutomaticallyStart() = + testScope.runTest { + val latest by collectLastValue(underTest.screenRecordState) + + // First recording starts, records, and stops + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(900) + advanceTimeBy(900) + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording + advanceTimeBy(5000) + screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing + advanceTimeBy(10000) + assertThat(latest).isEqualTo(ScreenRecordChipModel.DoingNothing) + + // WHEN a second recording is starting + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2900) + + // THEN we stay as starting and do not switch to Recording (verifying the auto-start + // timer is reset) + assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(2900)) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) + fun screenRecordState_startingButThenDoingNothing_doesNotAutomaticallyStart() = + testScope.runTest { + val latest by collectLastValue(underTest.screenRecordState) + + // WHEN a screen recording is starting in 500ms + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(500) + assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(500)) + + // But it's cancelled after 300ms + advanceTimeBy(300) + screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing + + // THEN we don't automatically start the recording 200ms later + advanceTimeBy(201) + assertThat(latest).isEqualTo(ScreenRecordChipModel.DoingNothing) + } + + @Test + @EnableFlags(FLAG_STATUS_BAR_AUTO_START_SCREEN_RECORD_CHIP) + fun screenRecordState_multipleStartingValues_autoStartResets() = + testScope.runTest { + val latest by collectLastValue(underTest.screenRecordState) + + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2900) + assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(2900)) + + advanceTimeBy(2800) + + // WHEN there's 100ms left to go before auto-start, but then we get a new start time + // that's in 500ms + screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(500) + + // THEN we don't auto-start in 100ms + advanceTimeBy(101) + assertThat(latest).isEqualTo(ScreenRecordChipModel.Starting(500)) + + // THEN we *do* auto-start 400ms later + advanceTimeBy(401) + assertThat(latest).isEqualTo(ScreenRecordChipModel.Recording(recordedTask = null)) + } + + @Test fun stopRecording_sendsToRepo() = testScope.runTest { assertThat(screenRecordRepo.stopRecordingInvoked).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt index bfebe184ae2d..48d8add6b33a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt @@ -26,9 +26,8 @@ import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager @@ -44,6 +43,7 @@ import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.getStopActionFromDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory +import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test @@ -61,7 +61,7 @@ import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class ScreenRecordChipViewModelTest : SysuiTestCase() { - private val kosmos = Kosmos().also { it.testCase = this } + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val screenRecordRepo = kosmos.screenRecordRepository private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository @@ -254,7 +254,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { MediaProjectionState.Projecting.SingleTask( "host.package", hostDeviceName = null, - FakeActivityTaskManager.createTask(taskId = 1) + FakeActivityTaskManager.createTask(taskId = 1), ) // THEN the start time is still the old start time @@ -275,12 +275,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockSystemUIDialog), - eq(chipBackgroundView), - any(), - anyBoolean(), - ) + .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean()) } @Test @@ -297,12 +292,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockSystemUIDialog), - eq(chipBackgroundView), - any(), - anyBoolean(), - ) + .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean()) } @Test @@ -314,7 +304,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { MediaProjectionState.Projecting.SingleTask( "host.package", hostDeviceName = null, - FakeActivityTaskManager.createTask(taskId = 1) + FakeActivityTaskManager.createTask(taskId = 1), ) val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener) @@ -323,12 +313,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { clickListener!!.onClick(chipView) // EndScreenRecordingDialogDelegate will test that the dialog has the right message verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - eq(mockSystemUIDialog), - eq(chipBackgroundView), - any(), - anyBoolean(), - ) + .showFromView(eq(mockSystemUIDialog), eq(chipBackgroundView), any(), anyBoolean()) } @Test @@ -344,12 +329,7 @@ class ScreenRecordChipViewModelTest : SysuiTestCase() { val cujCaptor = argumentCaptor<DialogCuj>() verify(kosmos.mockDialogTransitionAnimator) - .showFromView( - any(), - any(), - cujCaptor.capture(), - anyBoolean(), - ) + .showFromView(any(), any(), cujCaptor.capture(), anyBoolean()) assertThat(cujCaptor.firstValue.cujType) .isEqualTo(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt index e96def6d43a3..c5c2a94cf0ea 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt @@ -29,7 +29,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository @@ -48,6 +47,7 @@ import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel import com.android.systemui.statusbar.phone.ongoingcall.shared.model.inCallModel +import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -72,7 +72,7 @@ import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @DisableFlags(StatusBarNotifChips.FLAG_NAME) class OngoingActivityChipsViewModelTest : SysuiTestCase() { - private val kosmos = Kosmos().also { it.testCase = this } + private val kosmos = testKosmos() private val testScope = kosmos.testScope private val systemClock = kosmos.fakeSystemClock diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt index 3b5d358f7c2c..c4b1b841c6a5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt @@ -16,8 +16,8 @@ package com.android.systemui.statusbar.notification +import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableLooper -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase @@ -26,11 +26,17 @@ import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.flags.andSceneContainer import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.scene.data.repository.Idle +import com.android.systemui.scene.data.repository.setSceneTransition +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.ShadeViewController.Companion.WAKEUP_ANIMATION_DELAY_MS import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -41,9 +47,6 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,16 +58,21 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.anyFloat -import org.mockito.Mockito.clearInvocations -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @OptIn(ExperimentalCoroutinesApi::class) -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) -class NotificationWakeUpCoordinatorTest : SysuiTestCase() { +class NotificationWakeUpCoordinatorTest(flags: FlagsParameterization) : SysuiTestCase() { @get:Rule val animatorTestRule = AnimatorTestRule(this) @@ -105,6 +113,18 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { statusBarStateCallback.onDozeAmountChanged(dozeAmount, dozeAmount) } + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Before fun setup() { whenever(bypassController.bypassEnabled).then { bypassEnabled } @@ -178,6 +198,7 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun setDozeToZeroWhenCommunalShowingWillFullyHideNotifications() = testScope.runTest { val transitionState = @@ -192,6 +213,17 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { } @Test + @EnableSceneContainer + fun setDozeToZeroWhenCommunalShowingWillFullyHideNotifications_withSceneContainer() = + testScope.runTest { + kosmos.setSceneTransition(Idle(Scenes.Communal)) + setDozeAmount(0f) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue() + } + + @Test + @DisableSceneContainer fun closingCommunalWillShowNotifications() = testScope.runTest { val transitionState = @@ -211,6 +243,20 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { } @Test + @EnableSceneContainer + fun closingCommunalWillShowNotifications_withSceneContainer() = + testScope.runTest { + kosmos.setSceneTransition(Idle(Scenes.Communal)) + setDozeAmount(0f) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 1f, hideAmount = 1f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isTrue() + + kosmos.setSceneTransition(Idle(CommunalScenes.Blank)) + verifyStackScrollerDozeAndHideAmount(dozeAmount = 0f, hideAmount = 0f) + assertThat(notificationWakeUpCoordinator.notificationsFullyHidden).isFalse() + } + + @Test fun switchingToShadeWithBypassEnabledWillShowNotifications() { setDozeToZeroWithBypassWillFullyHideNotifications() clearInvocations(stackScrollerController) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt index 9dcbe1b591e5..ff0321b8cf2a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt @@ -99,6 +99,7 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { whenever(mainLooper.isCurrentThread).thenReturn(true) whenever(mainLooper.thread).thenReturn(thread) whenever(thread.name).thenReturn("backgroundThread") + whenever(context.applicationContext).thenReturn(context) whenever(context.resources).thenReturn(resources) whenever(context.mainExecutor).thenReturn(mContext.mainExecutor) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index b03c679a9c23..9b47eaddffd6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -17,13 +17,16 @@ package com.android.systemui.user.data.repository +import android.app.admin.devicePolicyManager import android.content.pm.UserInfo +import android.internal.statusbar.fakeStatusBarService import android.os.UserHandle import android.os.UserManager import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher @@ -57,6 +60,9 @@ class UserRepositoryImplTest : SysuiTestCase() { private val testDispatcher = kosmos.testDispatcher private val testScope = kosmos.testScope private val globalSettings = kosmos.fakeGlobalSettings + private val broadcastDispatcher = kosmos.broadcastDispatcher + private val devicePolicyManager = kosmos.devicePolicyManager + private val statusBarService = kosmos.fakeStatusBarService @Mock private lateinit var manager: UserManager @@ -317,6 +323,10 @@ class UserRepositoryImplTest : SysuiTestCase() { backgroundDispatcher = testDispatcher, globalSettings = globalSettings, tracker = tracker, + broadcastDispatcher = broadcastDispatcher, + devicePolicyManager = devicePolicyManager, + resources = context.resources, + statusBarService = statusBarService, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt new file mode 100644 index 000000000000..f70b42638cda --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.user.domain.interactor + +import android.content.pm.UserInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class UserLogoutInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + + private val userRepository = kosmos.fakeUserRepository + private val testScope = kosmos.testScope + + private val underTest = kosmos.userLogoutInteractor + + @Before + fun setUp() { + userRepository.setUserInfos(USER_INFOS) + runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[2]) } + userRepository.setLogoutToSystemUserEnabled(false) + userRepository.setSecondaryUserLogoutEnabled(false) + } + + @Test + fun logOut_doesNothing_whenBothLogoutOptionsAreDisabled() { + testScope.runTest { + val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled) + val secondaryUserLogoutCount = userRepository.logOutSecondaryUserCallCount + val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount + assertThat(isLogoutEnabled).isFalse() + underTest.logOut() + assertThat(userRepository.logOutSecondaryUserCallCount) + .isEqualTo(secondaryUserLogoutCount) + assertThat(userRepository.logOutToSystemUserCallCount) + .isEqualTo(logoutToSystemUserCount) + } + } + + @Test + fun logOut_logsOutSecondaryUser_whenAdminEnabledSecondaryLogout() { + testScope.runTest { + val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled) + val lastLogoutCount = userRepository.logOutSecondaryUserCallCount + val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount + userRepository.setSecondaryUserLogoutEnabled(true) + assertThat(isLogoutEnabled).isTrue() + underTest.logOut() + assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount + 1) + assertThat(userRepository.logOutToSystemUserCallCount) + .isEqualTo(logoutToSystemUserCount) + } + } + + @Test + fun logOut_logsOutToSystemUser_whenLogoutToSystemUserIsEnabled() { + testScope.runTest { + val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled) + val lastLogoutCount = userRepository.logOutSecondaryUserCallCount + val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount + userRepository.setLogoutToSystemUserEnabled(true) + assertThat(isLogoutEnabled).isTrue() + underTest.logOut() + assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount) + assertThat(userRepository.logOutToSystemUserCallCount) + .isEqualTo(logoutToSystemUserCount + 1) + } + } + + @Test + fun logOut_secondaryUserTakesPrecedence() { + testScope.runTest { + val isLogoutEnabled by collectLastValue(underTest.isLogoutEnabled) + val lastLogoutCount = userRepository.logOutSecondaryUserCallCount + val logoutToSystemUserCount = userRepository.logOutToSystemUserCallCount + userRepository.setLogoutToSystemUserEnabled(true) + userRepository.setSecondaryUserLogoutEnabled(true) + assertThat(isLogoutEnabled).isTrue() + underTest.logOut() + assertThat(userRepository.logOutSecondaryUserCallCount).isEqualTo(lastLogoutCount + 1) + assertThat(userRepository.logOutToSystemUserCallCount) + .isEqualTo(logoutToSystemUserCount) + } + } + + companion object { + private val USER_INFOS = + listOf( + UserInfo(0, "System user", 0), + UserInfo(10, "Regular user", 0), + UserInfo(11, "Secondary user", 0), + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt index e88dbd27fd37..ad473c09cf02 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepositoryTest.kt @@ -16,152 +16,16 @@ package com.android.systemui.util.settings.repository -import android.content.pm.UserInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.coroutines.collectValues -import com.android.systemui.kosmos.testScope -import com.android.systemui.testKosmos -import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository -import com.android.systemui.util.settings.fakeSettings -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) -class UserAwareSecureSettingsRepositoryTest : SysuiTestCase() { +class UserAwareSecureSettingsRepositoryTest : UserAwareSettingsRepositoryTestBase() { - private val kosmos = testKosmos() - private val testScope = kosmos.testScope - private val secureSettings = kosmos.fakeSettings - private val userRepository = kosmos.fakeUserRepository - private lateinit var underTest: UserAwareSecureSettingsRepository - - @Before - fun setup() { - underTest = kosmos.userAwareSecureSettingsRepository - - userRepository.setUserInfos(USER_INFOS) - - secureSettings.putBoolForUser(BOOL_SETTING_NAME, true, USER_1.id) - secureSettings.putBoolForUser(BOOL_SETTING_NAME, false, USER_2.id) - secureSettings.putIntForUser(INT_SETTING_NAME, 1337, USER_1.id) - secureSettings.putIntForUser(INT_SETTING_NAME, 818, USER_2.id) - } - - @Test - fun boolSetting_emitsInitialValue() { - testScope.runTest { - userRepository.setSelectedUserInfo(USER_1) - - val enabled by collectLastValue(underTest.boolSetting(BOOL_SETTING_NAME, false)) - - assertThat(enabled).isTrue() - } - } - - @Test - fun boolSetting_whenSettingChanges_emitsNewValue() { - testScope.runTest { - userRepository.setSelectedUserInfo(USER_1) - val enabled by collectValues(underTest.boolSetting(BOOL_SETTING_NAME, false)) - runCurrent() - - secureSettings.putBoolForUser(BOOL_SETTING_NAME, false, USER_1.id) - - assertThat(enabled).containsExactly(true, false).inOrder() - } - } - - @Test - fun boolSetting_whenWhenUserChanges_emitsNewValue() { - testScope.runTest { - userRepository.setSelectedUserInfo(USER_1) - val enabled by collectLastValue(underTest.boolSetting(BOOL_SETTING_NAME, false)) - runCurrent() - - userRepository.setSelectedUserInfo(USER_2) - - assertThat(enabled).isFalse() - } - } - - @Test - fun intSetting_emitsInitialValue() { - testScope.runTest { - userRepository.setSelectedUserInfo(USER_1) - - val number by collectLastValue(underTest.intSetting(INT_SETTING_NAME, 0)) - - assertThat(number).isEqualTo(1337) - } - } - - @Test - fun intSetting_whenSettingChanges_emitsNewValue() { - testScope.runTest { - userRepository.setSelectedUserInfo(USER_1) - val number by collectValues(underTest.intSetting(INT_SETTING_NAME, 0)) - runCurrent() - - secureSettings.putIntForUser(INT_SETTING_NAME, 1338, USER_1.id) - - assertThat(number).containsExactly(1337, 1338).inOrder() - } - } - - @Test - fun intSetting_whenWhenUserChanges_emitsNewValue() { - testScope.runTest { - userRepository.setSelectedUserInfo(USER_1) - val number by collectLastValue(underTest.intSetting(INT_SETTING_NAME, 0)) - runCurrent() - - userRepository.setSelectedUserInfo(USER_2) - - assertThat(number).isEqualTo(818) - } - } - - @Test - fun getInt_returnsInitialValue() = - testScope.runTest { - userRepository.setSelectedUserInfo(USER_1) - - assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(1337) - } - - @Test - fun getInt_whenSettingChanges_returnsNewValue() = - testScope.runTest { - userRepository.setSelectedUserInfo(USER_1) - secureSettings.putIntForUser(INT_SETTING_NAME, 999, USER_1.id) - - assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(999) - } - - @Test - fun getInt_whenUserChanges_returnsThatUserValue() = - testScope.runTest { - userRepository.setSelectedUserInfo(USER_2) - - assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(818) - } - - private companion object { - const val BOOL_SETTING_NAME = "BOOL_SETTING_NAME" - const val INT_SETTING_NAME = "INT_SETTING_NAME" - val USER_1 = UserInfo(/* id= */ 0, "user1", /* flags= */ 0) - val USER_2 = UserInfo(/* id= */ 1, "user2", /* flags= */ 0) - val USER_INFOS = listOf(USER_1, USER_2) + override fun getKosmosUserAwareSettingsRepository(): UserAwareSettingsRepository { + return kosmos.userAwareSecureSettingsRepository } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepositoryTestBase.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepositoryTestBase.kt new file mode 100644 index 000000000000..09db96f8ffb8 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepositoryTestBase.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.settings.repository + +import android.content.pm.UserInfo +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +abstract class UserAwareSettingsRepositoryTestBase : SysuiTestCase() { + + protected val kosmos = testKosmos() + private val testScope = kosmos.testScope + protected val secureSettings = kosmos.fakeSettings + protected val userRepository = kosmos.fakeUserRepository + private lateinit var underTest: UserAwareSettingsRepository + + @Before + fun setup() { + underTest = getKosmosUserAwareSettingsRepository() + + userRepository.setUserInfos(USER_INFOS) + + secureSettings.putBoolForUser(BOOL_SETTING_NAME, true, USER_1.id) + secureSettings.putBoolForUser(BOOL_SETTING_NAME, false, USER_2.id) + secureSettings.putIntForUser(INT_SETTING_NAME, 1337, USER_1.id) + secureSettings.putIntForUser(INT_SETTING_NAME, 818, USER_2.id) + } + + abstract fun getKosmosUserAwareSettingsRepository(): UserAwareSettingsRepository + + @Test + fun boolSetting_emitsInitialValue() { + testScope.runTest { + userRepository.setSelectedUserInfo(USER_1) + + val enabled by collectLastValue(underTest.boolSetting(BOOL_SETTING_NAME, false)) + + assertThat(enabled).isTrue() + } + } + + @Test + fun boolSetting_whenSettingChanges_emitsNewValue() { + testScope.runTest { + userRepository.setSelectedUserInfo(USER_1) + val enabled by collectValues(underTest.boolSetting(BOOL_SETTING_NAME, false)) + runCurrent() + + secureSettings.putBoolForUser(BOOL_SETTING_NAME, false, USER_1.id) + + assertThat(enabled).containsExactly(true, false).inOrder() + } + } + + @Test + fun boolSetting_whenWhenUserChanges_emitsNewValue() { + testScope.runTest { + userRepository.setSelectedUserInfo(USER_1) + val enabled by collectLastValue(underTest.boolSetting(BOOL_SETTING_NAME, false)) + runCurrent() + + userRepository.setSelectedUserInfo(USER_2) + + assertThat(enabled).isFalse() + } + } + + @Test + fun intSetting_emitsInitialValue() { + testScope.runTest { + userRepository.setSelectedUserInfo(USER_1) + + val number by collectLastValue(underTest.intSetting(INT_SETTING_NAME, 0)) + + assertThat(number).isEqualTo(1337) + } + } + + @Test + fun intSetting_whenSettingChanges_emitsNewValue() { + testScope.runTest { + userRepository.setSelectedUserInfo(USER_1) + val number by collectValues(underTest.intSetting(INT_SETTING_NAME, 0)) + runCurrent() + + secureSettings.putIntForUser(INT_SETTING_NAME, 1338, USER_1.id) + + assertThat(number).containsExactly(1337, 1338).inOrder() + } + } + + @Test + fun intSetting_whenWhenUserChanges_emitsNewValue() { + testScope.runTest { + userRepository.setSelectedUserInfo(USER_1) + val number by collectLastValue(underTest.intSetting(INT_SETTING_NAME, 0)) + runCurrent() + + userRepository.setSelectedUserInfo(USER_2) + + assertThat(number).isEqualTo(818) + } + } + + @Test + fun getInt_returnsInitialValue() = + testScope.runTest { + userRepository.setSelectedUserInfo(USER_1) + + assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(1337) + } + + @Test + fun getInt_whenSettingChanges_returnsNewValue() = + testScope.runTest { + userRepository.setSelectedUserInfo(USER_1) + secureSettings.putIntForUser(INT_SETTING_NAME, 999, USER_1.id) + + assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(999) + } + + @Test + fun getInt_whenUserChanges_returnsThatUserValue() = + testScope.runTest { + userRepository.setSelectedUserInfo(USER_2) + + assertThat(underTest.getInt(INT_SETTING_NAME, 0)).isEqualTo(818) + } + + private companion object { + const val BOOL_SETTING_NAME = "BOOL_SETTING_NAME" + const val INT_SETTING_NAME = "INT_SETTING_NAME" + val USER_1 = UserInfo(/* id= */ 0, "user1", /* flags= */ 0) + val USER_2 = UserInfo(/* id= */ 1, "user2", /* flags= */ 0) + val USER_INFOS = listOf(USER_1, USER_2) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepositoryTest.kt new file mode 100644 index 000000000000..586da8e541f6 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepositoryTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.settings.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.util.settings.data.repository.userAwareSystemSettingsRepository +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class UserAwareSystemSettingsRepositoryTest : UserAwareSettingsRepositoryTestBase() { + + override fun getKosmosUserAwareSettingsRepository(): UserAwareSettingsRepository { + return kosmos.userAwareSystemSettingsRepository + } +} diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 82c8c44f1efe..0854eb46ffdd 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -1086,4 +1086,9 @@ enable the desktop specific features. --> <bool name="config_enableDesktopFeatureSet">false</bool> + + <!-- + Whether the user switching can only happen by logging out and going through the system user (login screen). + --> + <bool name="config_userSwitchingMustGoThroughLoginScreen">false</bool> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index d30f73f6e2a1..53ab686ff0d7 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -794,6 +794,7 @@ <!-- QuickSettings: Bluetooth secondary label shown when bluetooth is being enabled [CHAR LIMIT=NONE] --> <string name="quick_settings_bluetooth_secondary_label_transient">Turning on…</string> <!-- QuickSettings: Brightness [CHAR LIMIT=NONE] --> + <string name="quick_settings_brightness_unable_adjust_msg">Can\'t adjust brightness because it\'s being\n controlled by the top app</string> <!-- QuickSettings: Rotation Unlocked [CHAR LIMIT=NONE] --> <string name="quick_settings_rotation_unlocked_label">Auto-rotate</string> <!-- Accessibility label for Auto-ratate QuickSettings tile [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index f98890ec9c5d..8ca0e807b31c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -219,7 +219,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private static final int MSG_USER_UNLOCKED = 334; private static final int MSG_ASSISTANT_STACK_CHANGED = 335; private static final int MSG_BIOMETRIC_AUTHENTICATION_CONTINUE = 336; - private static final int MSG_DEVICE_POLICY_MANAGER_STATE_CHANGED = 337; private static final int MSG_TELEPHONY_CAPABLE = 338; private static final int MSG_TIMEZONE_UPDATE = 339; private static final int MSG_USER_STOPPED = 340; @@ -402,7 +401,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab protected int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; private boolean mFingerprintDetectRunning; private boolean mIsDreaming; - private boolean mLogoutEnabled; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private final FingerprintInteractiveToAuthProvider mFingerprintInteractiveToAuthProvider; @@ -1739,9 +1737,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mHandler.obtainMessage(MSG_SERVICE_STATE_CHANGE, subId, 0, serviceState)); } else if (TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED.equals(action)) { mHandler.sendEmptyMessage(MSG_SIM_SUBSCRIPTION_INFO_CHANGED); - } else if (DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED.equals( - action)) { - mHandler.sendEmptyMessage(MSG_DEVICE_POLICY_MANAGER_STATE_CHANGED); } } }; @@ -2328,9 +2323,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab case MSG_BIOMETRIC_AUTHENTICATION_CONTINUE: updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); break; - case MSG_DEVICE_POLICY_MANAGER_STATE_CHANGED: - updateLogoutEnabled(); - break; case MSG_TELEPHONY_CAPABLE: updateTelephonyCapable((boolean) msg.obj); break; @@ -2496,7 +2488,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab boolean isUserUnlocked = mUserManager.isUserUnlocked(user); mLogger.logUserUnlockedInitialState(user, isUserUnlocked); mUserIsUnlocked.put(user, isUserUnlocked); - mLogoutEnabled = mDevicePolicyManager.isLogoutEnabled(); updateSecondaryLockscreenRequirement(user); List<UserInfo> allUsers = mUserManager.getUsers(); for (UserInfo userInfo : allUsers) { @@ -4060,28 +4051,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return null; // not found } - /** - * @return a cached version of DevicePolicyManager.isLogoutEnabled() - */ - public boolean isLogoutEnabled() { - return mLogoutEnabled; - } - - private void updateLogoutEnabled() { - Assert.isMainThread(); - boolean logoutEnabled = mDevicePolicyManager.isLogoutEnabled(); - if (mLogoutEnabled != logoutEnabled) { - mLogoutEnabled = logoutEnabled; - - for (int i = 0; i < mCallbacks.size(); i++) { - KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); - if (cb != null) { - cb.onLogoutEnabledChanged(); - } - } - } - } - protected int getBiometricLockoutDelay() { return BIOMETRIC_LOCKOUT_RESET_DELAY_MS; } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index 7ac5ac229793..fdee21bcc479 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -286,11 +286,6 @@ public class KeyguardUpdateMonitorCallback { public void onTrustAgentErrorMessage(CharSequence message) { } /** - * Called when a value of logout enabled is change. - */ - public void onLogoutEnabledChanged() { } - - /** * Called when authenticated fingerprint biometrics are cleared. */ public void onFingerprintsCleared() { } diff --git a/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt b/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt index 3270c71057b6..6c78b8b0e58a 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/data/repository/ScreenBrightnessRepository.kt @@ -64,6 +64,9 @@ interface ScreenBrightnessRepository { /** Current maximum value for the brightness */ val maxLinearBrightness: Flow<LinearBrightness> + /** Whether the current brightness value is overridden by the application window */ + val isBrightnessOverriddenByWindow: StateFlow<Boolean> + /** Gets the current values for min and max brightness */ suspend fun getMinMaxLinearBrightness(): Pair<LinearBrightness, LinearBrightness> @@ -178,6 +181,11 @@ constructor( .logDiffForTable(tableBuffer, TABLE_PREFIX_LINEAR, TABLE_COLUMN_BRIGHTNESS, null) .stateIn(applicationScope, SharingStarted.WhileSubscribed(), LinearBrightness(0f)) + override val isBrightnessOverriddenByWindow = brightnessInfo + .filterNotNull() + .map { it.isBrightnessOverrideByWindow } + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + override fun setTemporaryBrightness(value: LinearBrightness) { apiQueue.trySend(SetBrightnessMethod.Temporary(value)) } diff --git a/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt b/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt index 5647f521762f..b794c5c85645 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/domain/interactor/ScreenBrightnessInteractor.kt @@ -25,12 +25,12 @@ import com.android.systemui.brightness.shared.model.logDiffForTable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.table.TableLogBuffer -import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject /** * Converts between [GammaBrightness] and [LinearBrightness]. @@ -68,6 +68,8 @@ constructor( .stateIn(applicationScope, SharingStarted.WhileSubscribed(), GammaBrightness(0)) } + val brightnessOverriddenByWindow = screenBrightnessRepository.isBrightnessOverriddenByWindow + /** Sets the brightness temporarily, while the user is changing it. */ suspend fun setTemporaryBrightness(gammaBrightness: GammaBrightness) { screenBrightnessRepository.setTemporaryBrightness( diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 02161d255abc..917a4ff9036b 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -19,6 +19,7 @@ package com.android.systemui.brightness.ui.compose import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth @@ -27,8 +28,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -38,6 +41,7 @@ import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -62,6 +66,7 @@ import com.android.systemui.utils.PolicyRestriction @Composable private fun BrightnessSlider( + viewModel: BrightnessSliderViewModel, gammaValue: Int, valueRange: IntRange, label: Text.Resource, @@ -97,21 +102,31 @@ private fun BrightnessSlider( null } + val overriddenByAppState by if (Flags.showToastWhenAppControlBrightness()) { + viewModel.brightnessOverriddenByWindow.collectAsStateWithLifecycle() + } else { + mutableStateOf(false) + } + PlatformSlider( value = animatedValue, valueRange = floatValueRange, enabled = !isRestricted, onValueChange = { if (!isRestricted) { - hapticsViewModel?.onValueChange(it) - value = it.toInt() - onDrag(value) + if (!overriddenByAppState) { + hapticsViewModel?.onValueChange(it) + value = it.toInt() + onDrag(value) + } } }, onValueChangeFinished = { if (!isRestricted) { - hapticsViewModel?.onValueChangeEnded() - onStop(value) + if (!overriddenByAppState) { + hapticsViewModel?.onValueChangeEnded() + onStop(value) + } } }, modifier = @@ -136,6 +151,21 @@ private fun BrightnessSlider( }, interactionSource = interactionSource, ) + // Showing the warning toast if the current running app window has controlled the + // brightness value. + if (Flags.showToastWhenAppControlBrightness()) { + val context = LocalContext.current + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + if (interaction is DragInteraction.Start && overriddenByAppState) { + viewModel.showToast( + context, + R.string.quick_settings_brightness_unable_adjust_msg + ) + } + } + } + } } private val sliderBackgroundFrameSize = 8.dp @@ -167,6 +197,7 @@ fun BrightnessSliderContainer( Box(modifier = modifier.fillMaxWidth().sysuiResTag("brightness_slider")) { BrightnessSlider( + viewModel = viewModel, gammaValue = gamma, valueRange = viewModel.minBrightness.value..viewModel.maxBrightness.value, label = viewModel.label, diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt index a61ce8f62522..1630ee58449f 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt @@ -17,7 +17,8 @@ package com.android.systemui.brightness.ui.viewmodel import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue +import android.content.Context +import androidx.annotation.StringRes import com.android.systemui.brightness.domain.interactor.BrightnessPolicyEnforcementInteractor import com.android.systemui.brightness.domain.interactor.ScreenBrightnessInteractor import com.android.systemui.brightness.shared.model.GammaBrightness @@ -29,6 +30,7 @@ import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.res.R import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor +import com.android.systemui.settings.brightness.ui.BrightnessWarningToast import com.android.systemui.utils.PolicyRestriction import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -51,6 +53,7 @@ constructor( val hapticsViewModelFactory: SliderHapticsViewModel.Factory, private val brightnessMirrorShowingInteractor: BrightnessMirrorShowingInteractor, @Assisted private val supportsMirroring: Boolean, + private val brightnessWarningToast: BrightnessWarningToast, ) : ExclusiveActivatable() { private val hydrator = Hydrator("BrightnessSliderViewModel.hydrator") @@ -75,6 +78,15 @@ constructor( brightnessPolicyEnforcementInteractor.startAdminSupportDetailsDialog(restriction) } + val brightnessOverriddenByWindow = screenBrightnessInteractor.brightnessOverriddenByWindow + + fun showToast(viewContext: Context, @StringRes resId: Int) { + if (brightnessWarningToast.isToastActive()) { + return + } + brightnessWarningToast.show(viewContext, resId) + } + /** * As a brightness slider is dragged, the corresponding events should be sent using this method. */ diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 162047bb3b79..91b44e7a6202 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -36,7 +36,6 @@ import android.app.Dialog; import android.app.IActivityManager; import android.app.StatusBarManager; import android.app.WallpaperManager; -import android.app.admin.DevicePolicyManager; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -138,6 +137,7 @@ import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.android.systemui.user.domain.interactor.UserLogoutInteractor; import com.android.systemui.util.EmergencyDialerConstants; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.settings.GlobalSettings; @@ -197,7 +197,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final Context mContext; private final GlobalActionsManager mWindowManagerFuncs; private final AudioManager mAudioManager; - private final DevicePolicyManager mDevicePolicyManager; private final LockPatternUtils mLockPatternUtils; private final SelectedUserInteractor mSelectedUserInteractor; private final TelephonyListenerManager mTelephonyListenerManager; @@ -260,6 +259,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene private final ShadeController mShadeController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; private final DialogTransitionAnimator mDialogTransitionAnimator; + private final UserLogoutInteractor mLogoutInteractor; private final GlobalActionsInteractor mInteractor; @VisibleForTesting @@ -344,7 +344,6 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene Context context, GlobalActionsManager windowManagerFuncs, AudioManager audioManager, - DevicePolicyManager devicePolicyManager, LockPatternUtils lockPatternUtils, BroadcastDispatcher broadcastDispatcher, TelephonyListenerManager telephonyListenerManager, @@ -376,11 +375,11 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene KeyguardUpdateMonitor keyguardUpdateMonitor, DialogTransitionAnimator dialogTransitionAnimator, SelectedUserInteractor selectedUserInteractor, + UserLogoutInteractor logoutInteractor, GlobalActionsInteractor interactor) { mContext = context; mWindowManagerFuncs = windowManagerFuncs; mAudioManager = audioManager; - mDevicePolicyManager = devicePolicyManager; mLockPatternUtils = lockPatternUtils; mTelephonyListenerManager = telephonyListenerManager; mKeyguardStateController = keyguardStateController; @@ -412,6 +411,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene mKeyguardUpdateMonitor = keyguardUpdateMonitor; mDialogTransitionAnimator = dialogTransitionAnimator; mSelectedUserInteractor = selectedUserInteractor; + mLogoutInteractor = logoutInteractor; mInteractor = interactor; // receive broadcasts @@ -639,12 +639,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene } else if (GLOBAL_ACTION_KEY_SCREENSHOT.equals(actionKey)) { addIfShouldShowAction(tempActions, new ScreenshotAction()); } else if (GLOBAL_ACTION_KEY_LOGOUT.equals(actionKey)) { - // TODO(b/206032495): should call mDevicePolicyManager.getLogoutUserId() instead of - // hardcode it to USER_SYSTEM so it properly supports headless system user mode - // (and then call mDevicePolicyManager.clearLogoutUser() after switched) - if (mDevicePolicyManager.isLogoutEnabled() - && currentUser.get() != null - && currentUser.get().id != UserHandle.USER_SYSTEM) { + if (mLogoutInteractor.isLogoutEnabled().getValue()) { addIfShouldShowAction(tempActions, new LogoutAction()); } } else if (GLOBAL_ACTION_KEY_EMERGENCY.equals(actionKey)) { @@ -1134,7 +1129,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene // Add a little delay before executing, to give the dialog a chance to go away before // switching user mHandler.postDelayed(() -> { - mDevicePolicyManager.logoutUser(); + mLogoutInteractor.logOut(); }, mDialogPressDelay); } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt index 585f7edeb0f2..922bc15c0633 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt @@ -52,6 +52,7 @@ class StickyKeysRepositoryImpl constructor( private val inputManager: InputManager, @Background private val backgroundDispatcher: CoroutineDispatcher, + // TODO: b/377244768 - Change to inject SecureSettingsRepository secureSettingsRepository: UserAwareSecureSettingsRepository, private val stickyKeysLogger: StickyKeysLogger, ) : StickyKeysRepository { diff --git a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java index 765b45bdbf2e..bab88c0b0bf9 100644 --- a/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java +++ b/packages/SystemUI/src/com/android/systemui/qrcodescanner/controller/QRCodeScannerController.java @@ -159,7 +159,7 @@ public class QRCodeScannerController implements * Returns true if lock screen entry point for QR Code Scanner is to be enabled. */ public boolean isEnabledForLockScreenButton() { - return mQRCodeScannerEnabled && isAbleToLaunchScannerActivity() && isAllowedOnLockScreen(); + return isAbleToLaunchScannerActivity() && isAllowedOnLockScreen(); } /** Returns whether the QR scanner button is allowed on lockscreen. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index 624cf306a3b2..e912a0c7faa6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -63,6 +63,7 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository import com.android.systemui.util.LargeScreenUtils import com.android.systemui.util.asIndenting +import com.android.systemui.util.kotlin.emitOnStart import com.android.systemui.util.printSection import com.android.systemui.util.println import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow @@ -278,6 +279,24 @@ constructor( private val mediaSuddenlyAppearingInLandscape: Boolean get() = !qqsMediaInRow && qsMediaInRow + private val collapsedLandscapeMedia by + hydrator.hydratedStateOf( + traceName = "collapsedLandscapeMedia", + initialValue = resources.getBoolean(R.bool.config_quickSettingsMediaLandscapeCollapsed), + source = + configurationInteractor.onAnyConfigurationChange.emitOnStart().map { + resources.getBoolean(R.bool.config_quickSettingsMediaLandscapeCollapsed) + }, + ) + + private val qqsMediaExpansion: Float + get() = + if (qqsMediaInRow && collapsedLandscapeMedia) { + MediaHostState.COLLAPSED + } else { + MediaHostState.EXPANDED + } + private var qsBounds by mutableStateOf(Rect()) private val constrainedSquishinessFraction: Float @@ -379,6 +398,7 @@ constructor( initMediaHosts() // init regardless of using media (same as current QS). coroutineScope { launch { hydrateSquishinessInteractor() } + launch { hydrateQqsMediaExpansion() } launch { hydrator.activate() } launch { containerViewModel.activate() } launch { qqsMediaInRowViewModel.activate() } @@ -389,7 +409,7 @@ constructor( private fun initMediaHosts() { qqsMediaHost.apply { - expansion = MediaHostState.EXPANDED + expansion = qqsMediaExpansion showsOnlyActiveMedia = true init(MediaHierarchyManager.LOCATION_QQS) } @@ -405,6 +425,10 @@ constructor( .collect { squishinessInteractor.setSquishinessValue(it) } } + private suspend fun hydrateQqsMediaExpansion() { + snapshotFlow { qqsMediaExpansion }.collect { qqsMediaHost.expansion = it } + } + override fun dump(pw: PrintWriter, args: Array<out String>) { pw.asIndenting().run { printSection("Quick Settings state") { @@ -448,6 +472,8 @@ constructor( println("qqsMediaInRow", qqsMediaInRow) println("qsMediaVisible", qsMediaVisible) println("qsMediaInRow", qsMediaInRow) + println("collapsedLandscapeMedia", collapsedLandscapeMedia) + println("qqsMediaExpansion", qqsMediaExpansion) } } } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java index 649f8db89bc0..90d27f4b33e9 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java @@ -44,6 +44,7 @@ import android.util.MathUtils; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -56,6 +57,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.LogLevel; import com.android.systemui.log.core.LogMessage; +import com.android.systemui.res.R; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.util.settings.SecureSettings; @@ -111,6 +113,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig private boolean mControlValueInitialized; private float mBrightnessMin = PowerManager.BRIGHTNESS_MIN; private float mBrightnessMax = PowerManager.BRIGHTNESS_MAX; + private boolean mIsBrightnessOverriddenByWindow = false; private ValueAnimator mSliderAnimator; @@ -246,12 +249,14 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig @Override public void run() { final boolean inVrMode = mIsVrModeEnabled; - final BrightnessInfo info = mContext.getDisplay().getBrightnessInfo(); + final BrightnessInfo info = getBrightnessInfo(); if (info == null) { return; } mBrightnessMax = info.brightnessMaximum; mBrightnessMin = info.brightnessMinimum; + mIsBrightnessOverriddenByWindow = info.isBrightnessOverrideByWindow; + // Value is passed as intbits, since this is what the message takes. final int valueAsIntBits = Float.floatToIntBits(info.brightness); mMainHandler.obtainMessage(MSG_UPDATE_SLIDER, valueAsIntBits, @@ -353,7 +358,19 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig public void onChanged(boolean tracking, int value, boolean stopTracking) { boolean starting = !mTrackingTouch && tracking; mTrackingTouch = tracking; - if (mExternalChange) return; + if (starting) { + if (Flags.showToastWhenAppControlBrightness()) { + // Showing the warning toast if the current running app window has + // controlled the brightness value. + if (mIsBrightnessOverriddenByWindow) { + mControl.showToast(R.string.quick_settings_brightness_unable_adjust_msg); + } + } + } + if (mExternalChange + || (Flags.showToastWhenAppControlBrightness() && mIsBrightnessOverriddenByWindow)) { + return; + } if (mSliderAnimator != null) { mSliderAnimator.cancel(); @@ -424,6 +441,11 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig mDisplayManager.setTemporaryBrightness(mDisplayId, brightness); } + @VisibleForTesting + BrightnessInfo getBrightnessInfo() { + return mContext.getDisplay().getBrightnessInfo(); + } + private void updateVrMode(boolean isEnabled) { if (mIsVrModeEnabled != isEnabled) { mIsVrModeEnabled = isEnabled; diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java index 2f7df21893b7..3a90d2b9df7b 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessSliderController.java @@ -16,6 +16,7 @@ package com.android.systemui.settings.brightness; +import android.annotation.StringRes; import android.content.Context; import android.content.Intent; import android.view.LayoutInflater; @@ -36,6 +37,7 @@ import com.android.systemui.haptics.slider.HapticSliderViewBinder; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.res.R; +import com.android.systemui.settings.brightness.ui.BrightnessWarningToast; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.util.ViewController; @@ -68,6 +70,8 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV private final HapticSliderPlugin mBrightnessSliderHapticPlugin; private final ActivityStarter mActivityStarter; + private final BrightnessWarningToast mBrightnessWarningToast; + private final Gefingerpoken mOnInterceptListener = new Gefingerpoken() { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { @@ -90,12 +94,14 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV FalsingManager falsingManager, UiEventLogger uiEventLogger, HapticSliderPlugin brightnessSliderHapticPlugin, - ActivityStarter activityStarter) { + ActivityStarter activityStarter, + BrightnessWarningToast brightnessWarningToast) { super(brightnessSliderView); mFalsingManager = falsingManager; mUiEventLogger = uiEventLogger; mBrightnessSliderHapticPlugin = brightnessSliderHapticPlugin; mActivityStarter = activityStarter; + mBrightnessWarningToast = brightnessWarningToast; } /** @@ -225,6 +231,15 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV } @Override + public void showToast(@StringRes int resId) { + if (mBrightnessWarningToast.isToastActive()) { + return; + } + mBrightnessWarningToast.show(mView.getContext(), + R.string.quick_settings_brightness_unable_adjust_msg); + } + + @Override public boolean isVisible() { // this should be called rarely - once or twice per slider's value change, but not for // every value change when user slides finger - only the final one. @@ -286,6 +301,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV private final SystemClock mSystemClock; private final ActivityStarter mActivityStarter; private final MSDLPlayer mMSDLPlayer; + private final BrightnessWarningToast mBrightnessWarningToast; @Inject public Factory( @@ -294,7 +310,8 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV VibratorHelper vibratorHelper, MSDLPlayer msdlPlayer, SystemClock clock, - ActivityStarter activityStarter + ActivityStarter activityStarter, + BrightnessWarningToast brightnessWarningToast ) { mFalsingManager = falsingManager; mUiEventLogger = uiEventLogger; @@ -302,6 +319,7 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV mSystemClock = clock; mActivityStarter = activityStarter; mMSDLPlayer = msdlPlayer; + mBrightnessWarningToast = brightnessWarningToast; } /** @@ -323,8 +341,8 @@ public class BrightnessSliderController extends ViewController<BrightnessSliderV mSystemClock, new HapticSlider.SeekBar(root.requireViewById(R.id.slider))); HapticSliderViewBinder.bind(viewRoot, plugin); - return new BrightnessSliderController( - root, mFalsingManager, mUiEventLogger, plugin, mActivityStarter); + return new BrightnessSliderController(root, mFalsingManager, mUiEventLogger, plugin, + mActivityStarter, mBrightnessWarningToast); } /** Get the layout to inflate based on what slider to use */ diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java index 24bc67047a47..ed69d35e6053 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ToggleSlider.java @@ -16,6 +16,7 @@ package com.android.systemui.settings.brightness; +import android.annotation.StringRes; import android.view.MotionEvent; import com.android.settingslib.RestrictedLockUtils; @@ -37,5 +38,6 @@ public interface ToggleSlider { void showView(); void hideView(); + void showToast(@StringRes int resId); boolean isVisible(); } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt new file mode 100644 index 000000000000..dfbdaa62ec44 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/BrightnessWarningToast.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.settings.brightness.ui + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.StringRes +import android.content.Context +import android.graphics.PixelFormat +import android.view.Gravity +import android.view.View +import android.view.WindowManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.toast.ToastFactory +import javax.inject.Inject + +@SysUISingleton +class BrightnessWarningToast +@Inject +constructor( + private val toastFactory: ToastFactory, + private val windowManager: WindowManager, +) { + private var toastView: View? = null + + fun show(viewContext: Context, @StringRes resId: Int) { + val res = viewContext.resources + // Show the brightness warning toast with passing the toast inflation required context, + // userId and resId from SystemUI package. + val systemUIToast = toastFactory.createToast( + viewContext, + res.getString(resId), viewContext.packageName, viewContext.getUserId(), + res.configuration.orientation + ) + if (systemUIToast == null) { + return + } + + toastView = systemUIToast.view + + val params = WindowManager.LayoutParams() + params.height = WindowManager.LayoutParams.WRAP_CONTENT + params.width = WindowManager.LayoutParams.WRAP_CONTENT + params.format = PixelFormat.TRANSLUCENT + params.title = "Brightness warning toast" + params.type = WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL + params.flags = (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) + params.y = systemUIToast.yOffset + + val absGravity = Gravity.getAbsoluteGravity( + systemUIToast.gravity, + res.configuration.layoutDirection + ) + params.gravity = absGravity + if ((absGravity and Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { + params.horizontalWeight = TOAST_PARAMS_HORIZONTAL_WEIGHT + } + if ((absGravity and Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { + params.verticalWeight = TOAST_PARAMS_VERTICAL_WEIGHT + } + + windowManager.addView(toastView, params) + + val inAnimator = systemUIToast.inAnimation + inAnimator?.start() + + toastView!!.postDelayed({ + val outAnimator = systemUIToast.outAnimation + if (outAnimator != null) { + outAnimator.start() + outAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + windowManager.removeViewImmediate(toastView) + toastView = null + } + }) + } + }, TOAST_DURATION_MS) + } + + fun isToastActive(): Boolean { + return toastView != null && toastView!!.isAttachedToWindow + } + + companion object { + private const val TOAST_PARAMS_HORIZONTAL_WEIGHT = 1.0f + private const val TOAST_PARAMS_VERTICAL_WEIGHT = 1.0f + private const val TOAST_DURATION_MS: Long = 3000 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 8c5a711d6a75..a5595edcbb95 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -116,6 +116,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.KeyguardIndicationTextView; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.user.domain.interactor.UserLogoutInteractor; import com.android.systemui.util.AlarmTimeout; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.wakelock.SettableWakeLock; @@ -162,6 +163,7 @@ public class KeyguardIndicationController { private final KeyguardLogger mKeyguardLogger; private final UserTracker mUserTracker; private final BouncerMessageInteractor mBouncerMessageInteractor; + private ViewGroup mIndicationArea; private KeyguardIndicationTextView mTopIndicationView; private KeyguardIndicationTextView mLockScreenIndicationView; @@ -187,6 +189,7 @@ public class KeyguardIndicationController { private final BiometricMessageInteractor mBiometricMessageInteractor; private DeviceEntryFingerprintAuthInteractor mDeviceEntryFingerprintAuthInteractor; private DeviceEntryFaceAuthInteractor mDeviceEntryFaceAuthInteractor; + private final UserLogoutInteractor mUserLogoutInteractor; private String mPersistentUnlockMessage; private String mAlignmentIndication; private boolean mForceIsDismissible; @@ -237,6 +240,13 @@ public class KeyguardIndicationController { showTrustAgentErrorMessage(mTrustAgentErrorMessage); } }; + @VisibleForTesting + final Consumer<Boolean> mIsLogoutEnabledCallback = + (Boolean isLogoutEnabled) -> { + if (mVisible) { + updateDeviceEntryIndication(false); + } + }; private final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() { @Override public void onScreenTurnedOn() { @@ -299,7 +309,8 @@ public class KeyguardIndicationController { KeyguardInteractor keyguardInteractor, BiometricMessageInteractor biometricMessageInteractor, DeviceEntryFingerprintAuthInteractor deviceEntryFingerprintAuthInteractor, - DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor + DeviceEntryFaceAuthInteractor deviceEntryFaceAuthInteractor, + UserLogoutInteractor userLogoutInteractor ) { mContext = context; mBroadcastDispatcher = broadcastDispatcher; @@ -331,6 +342,8 @@ public class KeyguardIndicationController { mBiometricMessageInteractor = biometricMessageInteractor; mDeviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor; mDeviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor; + mUserLogoutInteractor = userLogoutInteractor; + mFaceAcquiredMessageDeferral = faceHelpMessageDeferral.create(); @@ -418,6 +431,9 @@ public class KeyguardIndicationController { mCoExAcquisitionMsgIdsToShowCallback); collectFlow(mIndicationArea, mDeviceEntryFingerprintAuthInteractor.isEngaged(), mIsFingerprintEngagedCallback); + collectFlow(mIndicationArea, + mUserLogoutInteractor.isLogoutEnabled(), + mIsLogoutEnabledCallback); } /** @@ -744,9 +760,7 @@ public class KeyguardIndicationController { } private void updateLockScreenLogoutView() { - final boolean shouldShowLogout = mKeyguardUpdateMonitor.isLogoutEnabled() - && getCurrentUser() != UserHandle.USER_SYSTEM; - if (shouldShowLogout) { + if (mUserLogoutInteractor.isLogoutEnabled().getValue()) { mRotateTextViewController.updateIndication( INDICATION_TYPE_LOGOUT, new KeyguardIndication.Builder() @@ -760,7 +774,7 @@ public class KeyguardIndicationController { if (mFalsingManager.isFalseTap(LOW_PENALTY)) { return; } - mDevicePolicyManager.logoutUser(); + mUserLogoutInteractor.logOut(); }) .build(), false); @@ -1515,13 +1529,6 @@ public class KeyguardIndicationController { } @Override - public void onLogoutEnabledChanged() { - if (mVisible) { - updateDeviceEntryIndication(false); - } - } - - @Override public void onRequireUnlockForNfc() { showTransientIndication(mContext.getString(R.string.require_unlock_for_nfc)); hideTransientIndicationDelayed(DEFAULT_HIDE_DELAY_MS); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java index 3a24ec9408ad..c1b8d9d123b9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java @@ -383,34 +383,20 @@ public class NotificationGroupingUtil { } protected boolean hasSameIcon(Object parentData, Object childData) { - Icon parentIcon = getIcon((Notification) parentData); - Icon childIcon = getIcon((Notification) childData); + Icon parentIcon = ((Notification) parentData).getSmallIcon(); + Icon childIcon = ((Notification) childData).getSmallIcon(); return parentIcon.sameAs(childIcon); } - private static Icon getIcon(Notification notification) { - if (notification.shouldUseAppIcon()) { - return notification.getAppIcon(); - } - return notification.getSmallIcon(); - } - /** * @return whether two ImageViews have the same colorFilterSet or none at all */ protected boolean hasSameColor(Object parentData, Object childData) { - int parentColor = getColor((Notification) parentData); - int childColor = getColor((Notification) childData); + int parentColor = ((Notification) parentData).color; + int childColor = ((Notification) childData).color; return parentColor == childColor; } - private static int getColor(Notification notification) { - if (notification.shouldUseAppIcon()) { - return 0; // the color filter isn't applied if using the app icon - } - return notification.color; - } - @Override public boolean isEmpty(View view) { return false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java index ad3afd4d1756..33f0c64269cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java @@ -27,7 +27,6 @@ import android.app.ActivityManager; import android.app.Notification; import android.content.Context; import android.content.pm.ActivityInfo; -import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; @@ -36,7 +35,6 @@ import android.graphics.Color; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.Rect; -import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Trace; @@ -520,34 +518,8 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi userId = UserHandle.USER_SYSTEM; } - // Try to load the monochrome app icon if applicable - Drawable icon = maybeGetMonochromeAppIcon(context, statusBarIcon); - // Otherwise, just use the icon normally - if (icon == null) { - icon = statusBarIcon.icon.loadDrawableAsUser(context, userId); - } - return icon; - } - } - - @Nullable - private Drawable maybeGetMonochromeAppIcon(Context context, - StatusBarIcon statusBarIcon) { - if (android.app.Flags.notificationsUseMonochromeAppIcon() - && statusBarIcon.type == StatusBarIcon.Type.MaybeMonochromeAppIcon) { - // Check if we have a monochrome app icon - PackageManager pm = context.getPackageManager(); - Drawable appIcon = context.getApplicationInfo().loadIcon(pm); - if (appIcon instanceof AdaptiveIconDrawable) { - Drawable monochrome = ((AdaptiveIconDrawable) appIcon).getMonochrome(); - if (monochrome != null) { - setCropToPadding(true); - setScaleType(ScaleType.CENTER); - return new ScalingDrawableWrapper(monochrome, APP_ICON_SCALE); - } - } + return statusBarIcon.icon.loadDrawableAsUser(context, userId); } - return null; } public StatusBarIcon getStatusBarIcon() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt index 9c53cc13f702..e3dc70af5fe6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.chips.screenrecord.domain.interactor +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer @@ -28,14 +29,19 @@ import com.android.systemui.statusbar.chips.StatusBarChipsLog import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch /** Interactor for the screen recording chip shown in the status bar. */ @SysUISingleton +@OptIn(ExperimentalCoroutinesApi::class) class ScreenRecordChipInteractor @Inject constructor( @@ -44,6 +50,32 @@ constructor( private val mediaProjectionRepository: MediaProjectionRepository, @StatusBarChipsLog private val logger: LogBuffer, ) { + /** + * Emits true if we should assume that we're currently screen recording, even if + * [ScreenRecordRepository.screenRecordState] hasn't emitted [ScreenRecordModel.Recording] yet. + */ + private val shouldAssumeIsRecording: Flow<Boolean> = + screenRecordRepository.screenRecordState + .transformLatest { + when (it) { + is ScreenRecordModel.DoingNothing -> { + emit(false) + } + is ScreenRecordModel.Starting -> { + // If we're told that the recording will start in [it.millisUntilStarted], + // optimistically assume the recording did indeed start after that time even + // if [ScreenRecordRepository.screenRecordState] hasn't emitted + // [ScreenRecordModel.Recording] yet. Start 50ms early so that the chip + // timer will definitely be showing by the time the recording actually + // starts - see b/366448907. + delay(it.millisUntilStarted - 50) + emit(true) + } + is ScreenRecordModel.Recording -> {} + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + val screenRecordState: StateFlow<ScreenRecordChipModel> = // ScreenRecordRepository has the main "is the screen being recorded?" state, and // MediaProjectionRepository has information about what specifically is being recorded (a @@ -51,37 +83,55 @@ constructor( combine( screenRecordRepository.screenRecordState, mediaProjectionRepository.mediaProjectionState, - ) { screenRecordState, mediaProjectionState -> - when (screenRecordState) { - is ScreenRecordModel.DoingNothing -> { - logger.log(TAG, LogLevel.INFO, {}, { "State: DoingNothing" }) - ScreenRecordChipModel.DoingNothing - } - is ScreenRecordModel.Starting -> { - logger.log( - TAG, - LogLevel.INFO, - { long1 = screenRecordState.millisUntilStarted }, - { "State: Starting($long1)" } - ) - ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted) - } - is ScreenRecordModel.Recording -> { - val recordedTask = - if ( - mediaProjectionState is MediaProjectionState.Projecting.SingleTask - ) { - mediaProjectionState.task - } else { - null - } - logger.log( - TAG, - LogLevel.INFO, - { str1 = recordedTask?.baseIntent?.component?.packageName }, - { "State: Recording(taskPackage=$str1)" } - ) - ScreenRecordChipModel.Recording(recordedTask) + shouldAssumeIsRecording, + ) { screenRecordState, mediaProjectionState, shouldAssumeIsRecording -> + if ( + Flags.statusBarAutoStartScreenRecordChip() && + shouldAssumeIsRecording && + screenRecordState is ScreenRecordModel.Starting + ) { + logger.log( + TAG, + LogLevel.INFO, + {}, + { "State: Recording(taskPackage=null) due to force-start" }, + ) + ScreenRecordChipModel.Recording(recordedTask = null) + } else { + when (screenRecordState) { + is ScreenRecordModel.DoingNothing -> { + logger.log(TAG, LogLevel.INFO, {}, { "State: DoingNothing" }) + ScreenRecordChipModel.DoingNothing + } + + is ScreenRecordModel.Starting -> { + logger.log( + TAG, + LogLevel.INFO, + { long1 = screenRecordState.millisUntilStarted }, + { "State: Starting($long1)" }, + ) + ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted) + } + + is ScreenRecordModel.Recording -> { + val recordedTask = + if ( + mediaProjectionState + is MediaProjectionState.Projecting.SingleTask + ) { + mediaProjectionState.task + } else { + null + } + logger.log( + TAG, + LogLevel.INFO, + { str1 = recordedTask?.baseIntent?.component?.packageName }, + { "State: Recording(taskPackage=$str1)" }, + ) + ScreenRecordChipModel.Recording(recordedTask) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt index 16d0cc42db7f..3c8c42f6b29d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconBuilder.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.icon import android.app.Notification import android.content.Context -import android.graphics.drawable.Drawable import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.contentDescForNotification @@ -30,15 +29,11 @@ class IconBuilder @Inject constructor(private val context: Context) { return StatusBarIconView( context, "${entry.sbn.packageName}/0x${Integer.toHexString(entry.sbn.id)}", - entry.sbn + entry.sbn, ) } fun getIconContentDescription(n: Notification): CharSequence { return contentDescForNotification(context, n) } - - fun getAppIcon(n: Notification): Drawable { - return n.loadHeaderAppIcon(context) - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index db804835f260..47171948f395 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -26,6 +26,7 @@ import android.os.Bundle import android.util.Log import android.view.View import android.widget.ImageView +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.app.tracing.traceSection import com.android.internal.statusbar.StatusBarIcon import com.android.systemui.Flags @@ -44,7 +45,6 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext /** @@ -152,13 +152,7 @@ constructor( setIcon(entry, sensitiveIconDescriptor, shelfIcon) setIcon(entry, sensitiveIconDescriptor, aodIcon) entry.icons = - IconPack.buildPack( - sbIcon, - sbChipIcon, - shelfIcon, - aodIcon, - entry.icons, - ) + IconPack.buildPack(sbIcon, sbChipIcon, shelfIcon, aodIcon, entry.icons) } catch (e: InflationException) { entry.icons = IconPack.buildEmptyPack(entry.icons) throw e @@ -182,7 +176,7 @@ constructor( Log.wtf( TAG, "Updating using the cache is not supported when the " + - "notifications_background_icons flag is off" + "notifications_background_icons flag is off", ) } if (!usingCache || !Flags.notificationsBackgroundIcons()) { @@ -249,10 +243,6 @@ constructor( val (icon: Icon?, type: StatusBarIcon.Type) = if (showPeopleAvatar) { createPeopleAvatar(entry) to StatusBarIcon.Type.PeopleAvatar - } else if ( - android.app.Flags.notificationsUseMonochromeAppIcon() && n.shouldUseAppIcon() - ) { - n.smallIcon to StatusBarIcon.Type.MaybeMonochromeAppIcon } else { n.smallIcon to StatusBarIcon.Type.NotifSmallIcon } @@ -267,33 +257,25 @@ constructor( private fun getCachedIconDescriptor( entry: NotificationEntry, - showPeopleAvatar: Boolean + showPeopleAvatar: Boolean, ): StatusBarIcon? { val peopleAvatarDescriptor = entry.icons.peopleAvatarDescriptor - val appIconDescriptor = entry.icons.appIconDescriptor val smallIconDescriptor = entry.icons.smallIconDescriptor // If cached, return corresponding cached values return when { showPeopleAvatar && peopleAvatarDescriptor != null -> peopleAvatarDescriptor - android.app.Flags.notificationsUseMonochromeAppIcon() && appIconDescriptor != null -> - appIconDescriptor smallIconDescriptor != null -> smallIconDescriptor else -> null } } private fun cacheIconDescriptor(entry: NotificationEntry, descriptor: StatusBarIcon) { - if ( - android.app.Flags.notificationsUseAppIcon() || - android.app.Flags.notificationsUseMonochromeAppIcon() - ) { - // If either of the new icon flags is enabled, we cache the icon all the time. + if (android.app.Flags.notificationsRedesignAppIcons()) { + // Although we're not actually using the app icon in the status bar, let's make sure + // we cache the icon all the time when the flag is on. when (descriptor.type) { StatusBarIcon.Type.PeopleAvatar -> entry.icons.peopleAvatarDescriptor = descriptor - // When notificationsUseMonochromeAppIcon is enabled, we use the appIconDescriptor. - StatusBarIcon.Type.MaybeMonochromeAppIcon -> - entry.icons.appIconDescriptor = descriptor // When notificationsUseAppIcon is enabled, the app icon overrides the small icon. // But either way, it's a good idea to cache the descriptor. else -> entry.icons.smallIconDescriptor = descriptor @@ -312,7 +294,7 @@ constructor( private fun setIcon( entry: NotificationEntry, iconDescriptor: StatusBarIcon, - iconView: StatusBarIconView + iconView: StatusBarIconView, ) { iconView.setShowsConversation(showsConversation(entry, iconView, iconDescriptor)) iconView.setTag(R.id.icon_is_pre_L, entry.targetSdk < Build.VERSION_CODES.LOLLIPOP) @@ -323,7 +305,7 @@ constructor( private fun Icon.toStatusBarIcon( entry: NotificationEntry, - type: StatusBarIcon.Type + type: StatusBarIcon.Type, ): StatusBarIcon { val n = entry.sbn.notification return StatusBarIcon( @@ -333,7 +315,7 @@ constructor( n.iconLevel, n.number, iconBuilder.getIconContentDescription(n), - type + type, ) } @@ -347,7 +329,7 @@ constructor( } catch (e: Exception) { Log.e( TAG, - "Error calling LauncherApps#getShortcutIcon for notification $entry: $e" + "Error calling LauncherApps#getShortcutIcon for notification $entry: $e", ) } } @@ -431,7 +413,7 @@ constructor( private fun showsConversation( entry: NotificationEntry, iconView: StatusBarIconView, - iconDescriptor: StatusBarIcon + iconDescriptor: StatusBarIcon, ): Boolean { val usedInSensitiveContext = iconView === entry.icons.shelfIcon || iconView === entry.icons.aodIcon diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java index 611cebcf6427..cb6be661c7f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconPack.java @@ -34,7 +34,6 @@ public final class IconPack { @Nullable private final StatusBarIconView mAodIcon; @Nullable private StatusBarIcon mSmallIconDescriptor; - @Nullable private StatusBarIcon mAppIconDescriptor; @Nullable private StatusBarIcon mPeopleAvatarDescriptor; private boolean mIsImportantConversation; @@ -127,15 +126,6 @@ public final class IconPack { mPeopleAvatarDescriptor = peopleAvatarDescriptor; } - @Nullable - StatusBarIcon getAppIconDescriptor() { - return mAppIconDescriptor; - } - - void setAppIconDescriptor(@Nullable StatusBarIcon appIconDescriptor) { - mAppIconDescriptor = appIconDescriptor; - } - boolean isImportantConversation() { return mIsImportantConversation; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt index 6b5642af3f10..83f56a092bc6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/StatusBarIconViewBinder.kt @@ -20,7 +20,6 @@ import android.graphics.Rect import android.view.View import com.android.app.tracing.traceSection import com.android.internal.util.ContrastColorUtil -import com.android.systemui.Flags import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.NO_COLOR @@ -36,11 +35,9 @@ object StatusBarIconViewBinder { suspend fun bindColor(view: StatusBarIconView, color: Flow<Int>) { color.collectTracingEach("SBIV#bindColor") { color -> - // Don't change the icon color if an app icon experiment is enabled. - if (!android.app.Flags.notificationsUseAppIcon()) { - view.staticDrawableColor = color - } - // Continue changing the overflow dot color + // Set the color for the icons + view.staticDrawableColor = color + // Set the color for the overflow dot view.setDecorColor(color) } } @@ -59,14 +56,12 @@ object StatusBarIconViewBinder { contrastColorUtil: ContrastColorUtil, ) { iconColors.collectTracingEach("SBIV#bindIconColors") { colors -> - // Don't change the icon color if an app icon experiment is enabled. - if (!android.app.Flags.notificationsUseAppIcon()) { - val isPreL = java.lang.Boolean.TRUE == view.getTag(R.id.icon_is_pre_L) - val isColorized = !isPreL || NotificationUtils.isGrayscale(view, contrastColorUtil) - view.staticDrawableColor = - if (isColorized) colors.staticDrawableColor(view.viewBounds) else NO_COLOR - } - // Continue changing the overflow dot color + // Set the icon color + val isPreL = java.lang.Boolean.TRUE == view.getTag(R.id.icon_is_pre_L) + val isColorized = !isPreL || NotificationUtils.isGrayscale(view, contrastColorUtil) + view.staticDrawableColor = + if (isColorized) colors.staticDrawableColor(view.viewBounds) else NO_COLOR + // Set the color for the overflow dot view.setDecorColor(colors.tint) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index f3521234e67a..b622defbef98 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -203,11 +203,7 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper imple addRemainingTransformTypes(); updateCropToPaddingForImageViews(); Notification n = row.getEntry().getSbn().getNotification(); - if (n.shouldUseAppIcon()) { - mIcon.setTag(ImageTransformState.ICON_TAG, n.getAppIcon()); - } else { - mIcon.setTag(ImageTransformState.ICON_TAG, n.getSmallIcon()); - } + mIcon.setTag(ImageTransformState.ICON_TAG, n.getSmallIcon()); // We need to reset all views that are no longer transforming in case a view was previously // transformed, but now we decided to transform its container instead. diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 493aa8c11b18..e9a33e062c60 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -19,12 +19,18 @@ package com.android.systemui.user.data.repository import android.annotation.SuppressLint import android.annotation.UserIdInt +import android.app.admin.DevicePolicyManager import android.content.Context +import android.content.IntentFilter import android.content.pm.UserInfo +import android.content.res.Resources import android.os.UserHandle import android.os.UserManager import android.provider.Settings import androidx.annotation.VisibleForTesting +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.internal.statusbar.IStatusBarService +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -38,6 +44,7 @@ import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -49,11 +56,12 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -100,6 +108,12 @@ interface UserRepository { /** Whether refresh users should be paused. */ var isRefreshUsersPaused: Boolean + /** Whether logout for secondary users is enabled by admin device policy. */ + val isSecondaryUserLogoutEnabled: StateFlow<Boolean> + + /** Whether logout into system user is enabled. */ + val isLogoutToSystemUserEnabled: StateFlow<Boolean> + /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */ fun refreshUsers() @@ -109,6 +123,12 @@ interface UserRepository { fun isUserSwitcherEnabled(): Boolean + /** Performs logout logout for secondary users. */ + suspend fun logOutSecondaryUser() + + /** Performs logout into the system user. */ + suspend fun logOutToSystemUser() + /** * Returns the user ID of the "main user" of the device. This user may have access to certain * features which are limited to at most one user. There will never be more than one main user @@ -131,12 +151,16 @@ class UserRepositoryImpl @Inject constructor( @Application private val appContext: Context, + @Main private val resources: Resources, private val manager: UserManager, @Application private val applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @Background private val backgroundDispatcher: CoroutineDispatcher, private val globalSettings: GlobalSettings, private val tracker: UserTracker, + private val devicePolicyManager: DevicePolicyManager, + private val broadcastDispatcher: BroadcastDispatcher, + private val statusBarService: IStatusBarService, ) : UserRepository { private val _userSwitcherSettings: StateFlow<UserSwitcherSettingsModel> = @@ -147,7 +171,7 @@ constructor( SETTING_SIMPLE_USER_SWITCHER, Settings.Global.ADD_USERS_WHEN_LOCKED, Settings.Global.USER_SWITCHER_ENABLED, - ), + ) ) .onStart { emit(Unit) } // Forces an initial update. .map { getSettings() } @@ -163,6 +187,7 @@ constructor( override var mainUserId: Int = UserHandle.USER_NULL private set + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_NULL private set @@ -221,12 +246,73 @@ constructor( .stateIn( applicationScope, SharingStarted.Eagerly, - initialValue = SelectedUserModel(tracker.userInfo, currentSelectionStatus) + initialValue = SelectedUserModel(tracker.userInfo, currentSelectionStatus), ) } override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo } + /** Whether the secondary user logout is enabled by the admin device policy. */ + private val isSecondaryUserLogoutSupported: Flow<Boolean> = + broadcastDispatcher + .broadcastFlow( + filter = + IntentFilter(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED) + ) { intent, _ -> + if ( + DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED == intent.action + ) { + Unit + } else { + null + } + } + .filterNotNull() + .onStart { emit(Unit) } + .map { _ -> devicePolicyManager.isLogoutEnabled() } + .flowOn(backgroundDispatcher) + + @SuppressLint("MissingPermission") + override val isSecondaryUserLogoutEnabled: StateFlow<Boolean> = + selectedUser + .flatMapLatestConflated { selectedUser -> + if (selectedUser.isEligibleForLogout()) { + isSecondaryUserLogoutSupported + } else { + flowOf(false) + } + } + .stateIn(applicationScope, SharingStarted.Eagerly, false) + + @SuppressLint("MissingPermission") + override val isLogoutToSystemUserEnabled: StateFlow<Boolean> = + selectedUser + .flatMapLatestConflated { selectedUser -> + if (selectedUser.isEligibleForLogout()) { + flowOf( + resources.getBoolean(R.bool.config_userSwitchingMustGoThroughLoginScreen) + ) + } else { + flowOf(false) + } + } + .stateIn(applicationScope, SharingStarted.Eagerly, false) + + @SuppressLint("MissingPermission") + override suspend fun logOutSecondaryUser() { + if (isSecondaryUserLogoutEnabled.value) { + withContext(backgroundDispatcher) { devicePolicyManager.logoutUser() } + } + } + + override suspend fun logOutToSystemUser() { + // TODO(b/377493351) : start using proper logout API once it is available. + // Using reboot is a temporary solution. + if (isLogoutToSystemUserEnabled.value) { + withContext(backgroundDispatcher) { statusBarService.reboot(false) } + } + } + @SuppressLint("MissingPermission") override fun refreshUsers() { applicationScope.launch { @@ -277,10 +363,7 @@ constructor( ) != 0 val isAddUsersFromLockscreen = - globalSettings.getInt( - Settings.Global.ADD_USERS_WHEN_LOCKED, - 0, - ) != 0 + globalSettings.getInt(Settings.Global.ADD_USERS_WHEN_LOCKED, 0) != 0 val isUserSwitcherEnabled = globalSettings.getInt( @@ -309,3 +392,11 @@ constructor( @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher" } } + +fun SelectedUserModel.isEligibleForLogout(): Boolean { + // TODO(b/206032495): should call mDevicePolicyManager.getLogoutUserId() instead of + // hardcode it to USER_SYSTEM so it properly supports headless system user mode + // (and then call mDevicePolicyManager.clearLogoutUser() after switched) + return selectionStatus == SelectionStatus.SELECTION_COMPLETE && + userInfo.id != android.os.UserHandle.USER_SYSTEM +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.kt new file mode 100644 index 000000000000..f2dd25fecf08 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLogoutInteractor.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.systemui.user.domain.interactor + +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn + +/** Encapsulates business logic to for the logout. */ +@SysUISingleton +class UserLogoutInteractor +@Inject +constructor( + private val userRepository: UserRepository, + @Application private val applicationScope: CoroutineScope, +) { + + val isLogoutEnabled: StateFlow<Boolean> = + combine( + userRepository.isSecondaryUserLogoutEnabled, + userRepository.isLogoutToSystemUserEnabled, + Boolean::or, + ) + .stateIn(applicationScope, SharingStarted.Eagerly, false) + + fun logOut() { + applicationScope.launch { + if (userRepository.isSecondaryUserLogoutEnabled.value) { + userRepository.logOutSecondaryUser() + } else if (userRepository.isLogoutToSystemUserEnabled.value) { + userRepository.logOutToSystemUser() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt index 71335ec84c86..bc3726d362e2 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSecureSettingsRepository.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.CoroutineDispatcher * Repository for observing values of [Settings.Secure] for the currently active user. That means * when user is switched and the new user has different value, flow will emit new value. */ +// TODO: b/377244768 - Make internal once call sites inject SecureSettingsRepository instead. @SysUISingleton class UserAwareSecureSettingsRepository @Inject diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt index a31b8d943a1b..49a0f14d6b3b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSettingsRepository.kt @@ -16,7 +16,6 @@ package com.android.systemui.util.settings.repository -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.settings.SettingsProxyExt.observerFlow @@ -34,10 +33,10 @@ import kotlinx.coroutines.withContext /** * Repository for observing values of a [UserSettingsProxy], for the currently active user. That - * means that when user is switched and the new user has a different value, the flow will emit the - * new value. + * means that when the user is switched and the new user has a different value, the flow will emit + * the new value. */ -@SysUISingleton +// TODO: b/377244768 - Make internal when UserAwareSecureSettingsRepository can be made internal. @OptIn(ExperimentalCoroutinesApi::class) abstract class UserAwareSettingsRepository( private val userSettings: UserSettingsProxy, diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt index 8b1fca551d51..4b01ded16495 100644 --- a/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/util/settings/repository/UserAwareSystemSettingsRepository.kt @@ -17,12 +17,10 @@ package com.android.systemui.util.settings.repository import android.provider.Settings -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.settings.SystemSettings -import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineDispatcher @@ -30,10 +28,8 @@ import kotlinx.coroutines.CoroutineDispatcher * Repository for observing values of [Settings.Secure] for the currently active user. That means * when user is switched and the new user has different value, flow will emit new value. */ -@SysUISingleton -class UserAwareSystemSettingsRepository -@Inject -constructor( +// TODO: b/377244768 - Make internal once call sites inject SystemSettingsRepository instead. +class UserAwareSystemSettingsRepository( systemSettings: SystemSettings, userRepository: UserRepository, @Background backgroundDispatcher: CoroutineDispatcher, diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java index df50f765349c..24bca70fd41f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogLiteTest.java @@ -31,7 +31,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.IActivityManager; -import android.app.admin.DevicePolicyManager; import android.app.trust.TrustManager; import android.content.pm.PackageManager; import android.content.pm.UserInfo; @@ -80,6 +79,7 @@ import com.android.systemui.statusbar.window.StatusBarWindowController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; +import com.android.systemui.user.domain.interactor.UserLogoutInteractor; import com.android.systemui.util.RingerModeLiveData; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.settings.FakeGlobalSettings; @@ -106,7 +106,6 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private GlobalActions.GlobalActionsManager mWindowManagerFuncs; @Mock private AudioManager mAudioManager; - @Mock private DevicePolicyManager mDevicePolicyManager; @Mock private LockPatternUtils mLockPatternUtils; @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock private TelephonyListenerManager mTelephonyListenerManager; @@ -140,6 +139,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private DialogTransitionAnimator mDialogTransitionAnimator; @Mock private SelectedUserInteractor mSelectedUserInteractor; + @Mock private UserLogoutInteractor mLogoutInteractor; @Mock private OnBackInvokedDispatcher mOnBackInvokedDispatcher; @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; @@ -166,7 +166,6 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mGlobalActionsDialogLite = new GlobalActionsDialogLite(mContext, mWindowManagerFuncs, mAudioManager, - mDevicePolicyManager, mLockPatternUtils, mBroadcastDispatcher, mTelephonyListenerManager, @@ -198,6 +197,7 @@ public class GlobalActionsDialogLiteTest extends SysuiTestCase { mKeyguardUpdateMonitor, mDialogTransitionAnimator, mSelectedUserInteractor, + mLogoutInteractor, mInteractor); mGlobalActionsDialogLite.setZeroDialogPressDelayForTesting(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt index ad5242e2e036..4546b995316a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/data/repository/FakeScreenBrightnessRepository.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.map class FakeScreenBrightnessRepository( initialBrightnessInfo: BrightnessInfo = - BrightnessInfo(0f, 0f, 1f, HIGH_BRIGHTNESS_MODE_OFF, 1f, BRIGHTNESS_MAX_REASON_NONE) + BrightnessInfo(0f, 0f, 1f, HIGH_BRIGHTNESS_MODE_OFF, 1f, BRIGHTNESS_MAX_REASON_NONE), ) : ScreenBrightnessRepository { private val brightnessInfo = MutableStateFlow(initialBrightnessInfo) @@ -36,6 +36,8 @@ class FakeScreenBrightnessRepository( override val linearBrightness = brightnessInfo.map { LinearBrightness(it.brightness) } override val minLinearBrightness = brightnessInfo.map { LinearBrightness(it.brightnessMinimum) } override val maxLinearBrightness = brightnessInfo.map { LinearBrightness(it.brightnessMaximum) } + override val isBrightnessOverriddenByWindow = + MutableStateFlow(initialBrightnessInfo.isBrightnessOverrideByWindow).asStateFlow() override suspend fun getMinMaxLinearBrightness(): Pair<LinearBrightness, LinearBrightness> { return minMaxLinearBrightness() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt index 52cdbed6e25d..2198e04eaf8a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.brightness.domain.interactor.screenBrightnessInterac import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor +import com.android.systemui.kosmos.brightnessWarningToast val Kosmos.brightnessSliderViewModelFactory: BrightnessSliderViewModel.Factory by Kosmos.Fixture { @@ -32,6 +33,7 @@ val Kosmos.brightnessSliderViewModelFactory: BrightnessSliderViewModel.Factory b hapticsViewModelFactory = sliderHapticsViewModelFactory, brightnessMirrorShowingInteractor = brightnessMirrorShowingInteractor, supportsMirroring = allowsMirroring, + brightnessWarningToast = brightnessWarningToast, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index 72cb1dfe38db..f43841b31c2e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -3,6 +3,9 @@ package com.android.systemui.kosmos import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.settings.brightness.ui.BrightnessWarningToast + +import com.android.systemui.util.mockito.mock import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.test.StandardTestDispatcher @@ -38,6 +41,9 @@ var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture { testScope.backgroundScope.coroutineContext } var Kosmos.mainCoroutineContext: CoroutineContext by Fixture { testScope.coroutineContext } +var Kosmos.brightnessWarningToast: BrightnessWarningToast by Kosmos.Fixture { + mock<BrightnessWarningToast>() +} /** * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelKosmos.kt index d1b613fe7f6e..f63698a3f2f9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/MediaInRowInLandscapeViewModelKosmos.kt @@ -21,6 +21,7 @@ import android.content.res.mainResources import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runCurrent import com.android.systemui.media.controls.ui.controller.mediaHostStatesManager import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment import com.android.systemui.shade.data.repository.shadeRepository @@ -56,4 +57,5 @@ fun Kosmos.setConfigurationForMediaInRow(mediaInRow: Boolean) { } mainResources.configuration.updateFrom(config) fakeConfigurationRepository.onConfigurationChange(config) + runCurrent() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt index 88063c9b5db9..aac122c6610c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/BrightnessSliderControllerKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.classifier.falsingManager import com.android.systemui.haptics.msdl.msdlPlayer import com.android.systemui.haptics.vibratorHelper import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.brightnessWarningToast import com.android.systemui.plugins.activityStarter import com.android.systemui.settings.brightness.BrightnessSliderController import com.android.systemui.util.time.systemClock @@ -35,5 +36,6 @@ var Kosmos.brightnessSliderControllerFactory by msdlPlayer, systemClock, activityStarter, + brightnessWarningToast, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index ed335f9a1834..85d582a27faf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -29,6 +29,7 @@ import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.yield @@ -67,6 +68,14 @@ class FakeUserRepository @Inject constructor() : UserRepository { ) override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo } + private val _isSecondaryUserLogoutEnabled = MutableStateFlow<Boolean>(false) + override val isSecondaryUserLogoutEnabled: StateFlow<Boolean> = + _isSecondaryUserLogoutEnabled.asStateFlow() + + private val _isLogoutToSystemUserEnabled = MutableStateFlow<Boolean>(false) + override val isLogoutToSystemUserEnabled: StateFlow<Boolean> = + _isLogoutToSystemUserEnabled.asStateFlow() + override var mainUserId: Int = MAIN_USER_ID override var lastSelectedNonGuestUserId: Int = mainUserId @@ -107,6 +116,28 @@ class FakeUserRepository @Inject constructor() : UserRepository { return _userSwitcherSettings.value.isUserSwitcherEnabled } + fun setSecondaryUserLogoutEnabled(logoutEnabled: Boolean) { + _isSecondaryUserLogoutEnabled.value = logoutEnabled + } + + var logOutSecondaryUserCallCount: Int = 0 + private set + + override suspend fun logOutSecondaryUser() { + logOutSecondaryUserCallCount++ + } + + fun setLogoutToSystemUserEnabled(logoutEnabled: Boolean) { + _isLogoutToSystemUserEnabled.value = logoutEnabled + } + + var logOutToSystemUserCallCount: Int = 0 + private set + + override suspend fun logOutToSystemUser() { + logOutToSystemUserCallCount++ + } + fun setUserInfos(infos: List<UserInfo>) { _userInfos.value = infos } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorKosmos.kt new file mode 100644 index 000000000000..d06e74468d3e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLogoutInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.user.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.user.data.repository.userRepository + +val Kosmos.userLogoutInteractor by + Kosmos.Fixture { + UserLogoutInteractor( + userRepository = userRepository, + applicationScope = applicationCoroutineScope, + ) + } diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt index 57b58d8741da..3a0f8c0b02fb 100644 --- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt +++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt @@ -306,7 +306,7 @@ constructor( } private fun isOnLargeScreen(): Boolean { - return context.resources.configuration.smallestScreenWidthDp > + return context.applicationContext.resources.configuration.smallestScreenWidthDp > INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java index d8f2b705d539..3ed8b0a748e1 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java @@ -16,7 +16,6 @@ package android.platform.test.ravenwood; import static android.os.Process.FIRST_APPLICATION_UID; -import static android.os.Process.SYSTEM_UID; import static android.os.UserHandle.SYSTEM; import android.annotation.NonNull; @@ -61,17 +60,14 @@ public final class RavenwoodConfig { * Unless the test author requests differently, run as "nobody", and give each collection of * tests its own unique PID. */ - int mUid = NOBODY_UID; + int mUid = FIRST_APPLICATION_UID; int mPid = sNextPid.getAndIncrement(); String mTestPackageName; String mTargetPackageName; - int mMinSdkLevel; int mTargetSdkLevel = Build.VERSION_CODES.CUR_DEVELOPMENT; - boolean mProvideMainThread = false; - final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties(); final List<Class<?>> mServicesRequired = new ArrayList<>(); @@ -108,20 +104,18 @@ public final class RavenwoodConfig { } /** - * Configure the identity of this process to be the system UID for the duration of the - * test. Has no effect on non-Ravenwood environments. + * @deprecated no longer used. We always use an app UID. */ + @Deprecated public Builder setProcessSystem() { - mConfig.mUid = SYSTEM_UID; return this; } /** - * Configure the identity of this process to be an app UID for the duration of the - * test. Has no effect on non-Ravenwood environments. + * @deprecated no longer used. We always use an app UID. */ + @Deprecated public Builder setProcessApp() { - mConfig.mUid = FIRST_APPLICATION_UID; return this; } @@ -144,14 +138,6 @@ public final class RavenwoodConfig { } /** - * Configure the min SDK level of the test. - */ - public Builder setMinSdkLevel(int sdkLevel) { - mConfig.mMinSdkLevel = sdkLevel; - return this; - } - - /** * Configure the target SDK level of the test. */ public Builder setTargetSdkLevel(int sdkLevel) { @@ -160,14 +146,10 @@ public final class RavenwoodConfig { } /** - * Configure a "main" thread to be available for the duration of the test, as defined - * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments. - * - * @deprecated + * @deprecated no longer used. Main thread is always available. */ @Deprecated public Builder setProvideMainThread(boolean provideMainThread) { - mConfig.mProvideMainThread = provideMainThread; return this; } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 3d6ac0f37050..bfa3802ce583 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -112,20 +112,18 @@ public final class RavenwoodRule implements TestRule { } /** - * Configure the identity of this process to be the system UID for the duration of the - * test. Has no effect on non-Ravenwood environments. + * @deprecated no longer used. We always use an app UID. */ + @Deprecated public Builder setProcessSystem() { - mBuilder.setProcessSystem(); return this; } /** - * Configure the identity of this process to be an app UID for the duration of the - * test. Has no effect on non-Ravenwood environments. + * @deprecated no longer used. We always use an app UID. */ + @Deprecated public Builder setProcessApp() { - mBuilder.setProcessApp(); return this; } @@ -139,14 +137,10 @@ public final class RavenwoodRule implements TestRule { } /** - * Configure a "main" thread to be available for the duration of the test, as defined - * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments. - * - * @deprecated + * @deprecated no longer used. Main thread is always available. */ @Deprecated public Builder setProvideMainThread(boolean provideMainThread) { - mBuilder.setProvideMainThread(provideMainThread); return this; } diff --git a/ravenwood/runtime-helper-src/framework/android/util/Log_host.java b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java index d232ef2076be..c85bd23db893 100644 --- a/ravenwood/runtime-helper-src/framework/android/util/Log_host.java +++ b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java @@ -18,6 +18,7 @@ package android.util; import android.util.Log.Level; import com.android.internal.os.RuntimeInit; +import com.android.ravenwood.common.RavenwoodCommonUtils; import java.io.PrintStream; @@ -35,6 +36,9 @@ public class Log_host { } public static int println_native(int bufID, int priority, String tag, String msg) { + if (priority < Log.INFO && !RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING) { + return msg.length(); // No verbose logging. + } final String buffer; switch (bufID) { case Log.LOG_ID_MAIN: buffer = "main"; break; diff --git a/services/art-profile b/services/art-profile index 6fa4c88cb1f6..ce1e2c6f1397 100644 --- a/services/art-profile +++ b/services/art-profile @@ -5657,7 +5657,7 @@ Lcom/android/server/utils/WatchedSparseSetArray; Lcom/android/server/utils/Watcher; Lcom/android/server/vibrator/VibratorController$NativeWrapper; Lcom/android/server/vibrator/VibratorController$OnVibrationCompleteListener; -Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener; +Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks; Lcom/android/server/vibrator/VibratorManagerService; Lcom/android/server/vr/EnabledComponentsObserver$EnabledComponentChangeListener; Lcom/android/server/vr/VrManagerService; diff --git a/services/art-wear-profile b/services/art-wear-profile index 47bdb1385137..1e3090f9bf00 100644 --- a/services/art-wear-profile +++ b/services/art-wear-profile @@ -1330,7 +1330,7 @@ Lcom/android/server/utils/WatchedSparseSetArray; Lcom/android/server/utils/Watcher; Lcom/android/server/vibrator/VibratorController$NativeWrapper; Lcom/android/server/vibrator/VibratorController$OnVibrationCompleteListener; -Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener; +Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks; Lcom/android/server/vibrator/VibratorManagerService; Lcom/android/server/vr/EnabledComponentsObserver$EnabledComponentChangeListener; Lcom/android/server/vr/VrManagerService; @@ -24948,7 +24948,7 @@ PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;-><init>()V PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->cancelSynced()V PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->getCapabilities()J PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->getVibratorIds()[I -PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->init(Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;)V +PLcom/android/server/vibrator/VibratorManagerService$NativeWrapper;->init(Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;)V PLcom/android/server/vibrator/VibratorManagerService$VibrationCompleteListener;-><init>(Lcom/android/server/vibrator/VibratorManagerService;)V PLcom/android/server/vibrator/VibratorManagerService$VibrationCompleteListener;->onComplete(IJ)V PLcom/android/server/vibrator/VibratorManagerService$VibrationRecords;-><init>(II)V diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b5dcdb1ac287..0826c5333e2f 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -1101,7 +1101,7 @@ public class ActivityManagerService extends IActivityManager.Stub ProfilingServiceHelper.getInstance().onProfilingTriggerOccurred( startInfo.getRealUid(), startInfo.getPackageName(), - ProfilingTrigger.TRIGGER_TYPE_APP_COLD_START_ACTIVITY); + ProfilingTrigger.TRIGGER_TYPE_APP_FULLY_DRAWN); } } }; diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java index 4ad7c10a1444..d2c044fdbb5e 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java +++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java @@ -255,6 +255,11 @@ final class DisplayDeviceInfo { public static final int DIFF_MODE_ID = 1 << 7; /** + * Diff result: The frame rate override list differs. + */ + public static final int DIFF_FRAME_RATE_OVERRIDE = 1 << 8; + + /** * Diff result: Catch-all for "everything changed" */ public static final int DIFF_EVERYTHING = 0XFFFFFFFF; @@ -523,6 +528,9 @@ final class DisplayDeviceInfo { if (modeId != other.modeId) { diff |= DIFF_MODE_ID; } + if (!Arrays.equals(frameRateOverrides, other.frameRateOverrides)) { + diff |= DIFF_FRAME_RATE_OVERRIDE; + } if (!Objects.equals(name, other.name) || !Objects.equals(uniqueId, other.uniqueId) || width != other.width @@ -546,7 +554,6 @@ final class DisplayDeviceInfo { || !Objects.equals(deviceProductInfo, other.deviceProductInfo) || ownerUid != other.ownerUid || !Objects.equals(ownerPackageName, other.ownerPackageName) - || !Arrays.equals(frameRateOverrides, other.frameRateOverrides) || !BrightnessSynchronizer.floatEquals(brightnessMinimum, other.brightnessMinimum) || !BrightnessSynchronizer.floatEquals(brightnessMaximum, other.brightnessMaximum) || !BrightnessSynchronizer.floatEquals(brightnessDefault, diff --git a/services/core/java/com/android/server/display/DisplayDeviceRepository.java b/services/core/java/com/android/server/display/DisplayDeviceRepository.java index 086f8a94d9b8..5f7bc4effa1b 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceRepository.java +++ b/services/core/java/com/android/server/display/DisplayDeviceRepository.java @@ -27,6 +27,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.server.display.DisplayManagerService.SyncRoot; import com.android.server.display.utils.DebugUtils; +import java.util.Arrays; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -177,18 +178,22 @@ class DisplayDeviceRepository implements DisplayAdapter.Listener { "handleDisplayDeviceChanged"); } int diff = device.mDebugLastLoggedDeviceInfo.diff(info); - if (diff == DisplayDeviceInfo.DIFF_STATE) { + if (diff == 0) { + Slog.i(TAG, "Display device same: " + info); + } else if (diff == DisplayDeviceInfo.DIFF_STATE) { Slog.i(TAG, "Display device changed state: \"" + info.name + "\", " + Display.stateToString(info.state)); } else if (diff == DisplayDeviceInfo.DIFF_ROTATION) { Slog.i(TAG, "Display device rotated: \"" + info.name + "\", " + Surface.rotationToString(info.rotation)); - } else if (diff - == (DisplayDeviceInfo.DIFF_MODE_ID | DisplayDeviceInfo.DIFF_RENDER_TIMINGS)) { + } else if ((diff & + (DisplayDeviceInfo.DIFF_MODE_ID | DisplayDeviceInfo.DIFF_RENDER_TIMINGS + | DisplayDeviceInfo.DIFF_FRAME_RATE_OVERRIDE)) != 0) { Slog.i(TAG, "Display device changed render timings: \"" + info.name + "\", renderFrameRate=" + info.renderFrameRate + ", presentationDeadlineNanos=" + info.presentationDeadlineNanos - + ", appVsyncOffsetNanos=" + info.appVsyncOffsetNanos); + + ", appVsyncOffsetNanos=" + info.appVsyncOffsetNanos + + ", frameRateOverrides=" + Arrays.toString(info.frameRateOverrides)); } else if (diff == DisplayDeviceInfo.DIFF_COMMITTED_STATE) { if (DEBUG) { Slog.i(TAG, "Display device changed committed state: \"" + info.name diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 0e77040187e1..5a2610b00772 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -96,6 +96,7 @@ import android.hardware.display.DisplayManagerGlobal; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayManagerInternal.DisplayGroupListener; import android.hardware.display.DisplayManagerInternal.DisplayTransactionListener; +import android.hardware.display.DisplayTopology; import android.hardware.display.DisplayViewport; import android.hardware.display.DisplayedContentSample; import android.hardware.display.DisplayedContentSamplingAttributes; @@ -118,6 +119,7 @@ import android.os.IBinder.DeathRecipient; import android.os.IThermalService; import android.os.Looper; import android.os.Message; +import android.os.PermissionEnforcer; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; @@ -4321,6 +4323,10 @@ public final class DisplayManagerService extends SystemService { @VisibleForTesting final class BinderService extends IDisplayManager.Stub { + BinderService() { + super(PermissionEnforcer.fromContext(getContext())); + } + /** * Returns information about the specified logical display. * @@ -5202,6 +5208,25 @@ public final class DisplayManagerService extends SystemService { } return ddc.getDefaultDozeBrightness(); } + + @EnforcePermission(MANAGE_DISPLAYS) + @Override // Binder call + public DisplayTopology getDisplayTopology() { + getDisplayTopology_enforcePermission(); + if (mDisplayTopologyCoordinator == null) { + return null; + } + return mDisplayTopologyCoordinator.getTopology(); + } + + @EnforcePermission(MANAGE_DISPLAYS) + @Override // Binder call + public void setDisplayTopology(DisplayTopology topology) { + setDisplayTopology_enforcePermission(); + if (mDisplayTopologyCoordinator != null) { + mDisplayTopologyCoordinator.setTopology(topology); + } + } } @VisibleForTesting diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index a9ed0aaf324b..c90dfbf5456e 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1595,7 +1595,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // Note throttling effectively changes the allowed brightness range, so, similarly to HBM, // we broadcast this change through setting. final float unthrottledBrightnessState = rawBrightnessState; - DisplayBrightnessState clampedState = mBrightnessClamperController.clamp(mPowerRequest, + DisplayBrightnessState clampedState = mBrightnessClamperController.clamp( + displayBrightnessState, mPowerRequest, brightnessState, slowChange, /* displayState= */ state); brightnessState = clampedState.getBrightness(); slowChange = clampedState.isSlowChange(); @@ -2003,7 +2004,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mCachedBrightnessInfo.brightnessMax.value, mCachedBrightnessInfo.hbmMode.value, mCachedBrightnessInfo.hbmTransitionPoint.value, - mCachedBrightnessInfo.brightnessMaxReason.value); + mCachedBrightnessInfo.brightnessMaxReason.value, + mCachedBrightnessInfo.brightnessReason.value + == BrightnessReason.REASON_OVERRIDE); } } @@ -2028,6 +2031,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call @BrightnessInfo.BrightnessMaxReason int maxReason = state != null ? state.getBrightnessMaxReason() : BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE; + BrightnessReason brightnessReason = state != null ? state.getBrightnessReason() + : new BrightnessReason(BrightnessReason.REASON_UNKNOWN); final float minBrightness = Math.max(stateMin, Math.min( mBrightnessRangeController.getCurrentBrightnessMin(), stateMax)); final float maxBrightness = Math.min( @@ -2055,6 +2060,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call changed |= mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason, maxReason); + changed |= + mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessReason, + brightnessReason.getReason()); return changed; } } @@ -2683,6 +2691,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call + mCachedBrightnessInfo.hbmTransitionPoint.value); pw.println(" mCachedBrightnessInfo.brightnessMaxReason =" + mCachedBrightnessInfo.brightnessMaxReason.value); + pw.println(" mCachedBrightnessInfo.brightnessReason =" + + mCachedBrightnessInfo.brightnessReason); } pw.println(" mDisplayBlanksAfterDozeConfig=" + mDisplayBlanksAfterDozeConfig); pw.println(" mBrightnessBucketsInDozeConfig=" + mBrightnessBucketsInDozeConfig); @@ -3390,6 +3400,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call new MutableFloat(HighBrightnessModeController.HBM_TRANSITION_POINT_INVALID); public MutableInt brightnessMaxReason = new MutableInt(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE); + public MutableInt brightnessReason = new MutableInt(BrightnessReason.REASON_UNKNOWN); public boolean checkAndSetFloat(MutableFloat mf, float f) { if (mf.value != f) { diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java index b101e5893b97..47226861545f 100644 --- a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java +++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java @@ -16,6 +16,7 @@ package com.android.server.display; +import android.hardware.display.DisplayTopology; import android.util.DisplayMetrics; import android.view.Display; import android.view.DisplayInfo; @@ -33,7 +34,7 @@ import java.util.function.BooleanSupplier; class DisplayTopologyCoordinator { @GuardedBy("mLock") - private final DisplayTopology mTopology; + private DisplayTopology mTopology; /** * Check if extended displays are enabled. If not, a topology is not needed. @@ -76,6 +77,21 @@ class DisplayTopologyCoordinator { } /** + * @return A deep copy of the topology. + */ + DisplayTopology getTopology() { + synchronized (mLock) { + return mTopology; + } + } + + void setTopology(DisplayTopology topology) { + synchronized (mLock) { + mTopology = topology; + } + } + + /** * Print the object's state and debug information into the given stream. * @param pw The stream to dump information to. */ @@ -108,6 +124,7 @@ class DisplayTopologyCoordinator { && info.displayGroupId == Display.DEFAULT_DISPLAY_GROUP; } + @VisibleForTesting static class Injector { DisplayTopology getTopology() { return new DisplayTopology(); diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java index a10094fdfbb8..6e579bf161ee 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java @@ -172,17 +172,18 @@ public class BrightnessClamperController { * Applies clamping * Called in DisplayControllerHandler */ - public DisplayBrightnessState clamp(DisplayManagerInternal.DisplayPowerRequest request, + public DisplayBrightnessState clamp(DisplayBrightnessState displayBrightnessState, + DisplayManagerInternal.DisplayPowerRequest request, float brightnessValue, boolean slowChange, int displayState) { float cappedBrightness = Math.min(brightnessValue, mBrightnessCap); - DisplayBrightnessState.Builder builder = DisplayBrightnessState.builder(); + DisplayBrightnessState.Builder builder = DisplayBrightnessState.Builder.from( + displayBrightnessState); builder.setIsSlowChange(slowChange); builder.setBrightness(cappedBrightness); builder.setMaxBrightness(mBrightnessCap); builder.setCustomAnimationRate(mCustomAnimationRate); builder.setBrightnessMaxReason(getBrightnessMaxReason()); - if (mClamperType != null) { builder.getBrightnessReason().addModifier(BrightnessReason.MODIFIER_THROTTLED); if (!mClamperApplied) { diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 794eb8754820..76e5ef011789 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -64,15 +64,12 @@ import android.provider.Settings; import android.service.dreams.DreamManagerInternal; import android.service.dreams.DreamService; import android.service.dreams.IDreamManager; -import android.text.TextUtils; import android.util.Slog; -import android.util.SparseArray; import android.view.Display; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.content.PackageMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLoggerImpl; import com.android.internal.util.DumpUtils; @@ -89,7 +86,6 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.CopyOnWriteArrayList; @@ -159,10 +155,6 @@ public final class DreamManagerService extends SystemService { private ComponentName mDreamOverlayServiceName; private final AmbientDisplayConfiguration mDozeConfig; - - /** Stores {@link PerUserPackageMonitor} to monitor dream uninstalls. */ - private final SparseArray<PackageMonitor> mPackageMonitors = new SparseArray<>(); - private final ActivityInterceptorCallback mActivityInterceptorCallback = new ActivityInterceptorCallback() { @Nullable @@ -226,15 +218,6 @@ public final class DreamManagerService extends SystemService { } } - private final class PerUserPackageMonitor extends PackageMonitor { - @Override - public void onPackageRemoved(String packageName, int uid) { - super.onPackageRemoved(packageName, uid); - final int userId = getChangingUserId(); - updateDreamOnPackageRemoved(packageName, userId); - } - } - public DreamManagerService(Context context) { this(context, new DreamHandler(FgThread.get().getLooper())); } @@ -350,33 +333,6 @@ public final class DreamManagerService extends SystemService { }); } - @Override - public void onUserStarting(@NonNull TargetUser user) { - super.onUserStarting(user); - mHandler.post(() -> { - final int userId = user.getUserIdentifier(); - if (!mPackageMonitors.contains(userId)) { - final PackageMonitor monitor = new PerUserPackageMonitor(); - monitor.register(mContext, UserHandle.of(userId), mHandler); - mPackageMonitors.put(userId, monitor); - } else { - Slog.w(TAG, "Package monitor already registered for " + userId); - } - }); - } - - @Override - public void onUserStopping(@NonNull TargetUser user) { - super.onUserStopping(user); - mHandler.post(() -> { - final PackageMonitor monitor = mPackageMonitors.removeReturnOld( - user.getUserIdentifier()); - if (monitor != null) { - monitor.unregister(); - } - }); - } - private void dumpInternal(PrintWriter pw) { synchronized (mLock) { pw.println("DREAM MANAGER (dumpsys dreams)"); @@ -708,22 +664,6 @@ public final class DreamManagerService extends SystemService { return validComponents.toArray(new ComponentName[validComponents.size()]); } - private void updateDreamOnPackageRemoved(String packageName, int userId) { - final ComponentName[] componentNames = componentsFromString( - Settings.Secure.getStringForUser(mContext.getContentResolver(), - Settings.Secure.SCREENSAVER_COMPONENTS, - userId)); - if (componentNames != null) { - // Filter out any components in the removed package. - final ComponentName[] filteredComponents = Arrays.stream(componentNames).filter( - (componentName -> !TextUtils.equals(componentName.getPackageName(), - packageName))).toArray(ComponentName[]::new); - if (filteredComponents.length != componentNames.length) { - setDreamComponentsForUser(userId, filteredComponents); - } - } - } - private void setDreamComponentsForUser(int userId, ComponentName[] componentNames) { Settings.Secure.putStringForUser(mContext.getContentResolver(), Settings.Secure.SCREENSAVER_COMPONENTS, diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java index 8cb51ce35a89..e545dd507f28 100644 --- a/services/core/java/com/android/server/input/InputGestureManager.java +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -323,26 +323,50 @@ final class InputGestureManager { return InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST; } customGestures.remove(data.getTrigger()); - if (customGestures.size() == 0) { + if (customGestures.isEmpty()) { mCustomInputGestures.remove(userId); } return InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS; } } - public void removeAllCustomInputGestures(int userId) { + public void removeAllCustomInputGestures(int userId, @Nullable InputGestureData.Filter filter) { synchronized (mGestureLock) { - mCustomInputGestures.remove(userId); + Map<InputGestureData.Trigger, InputGestureData> customGestures = + mCustomInputGestures.get(userId); + if (customGestures == null) { + return; + } + if (filter == null) { + mCustomInputGestures.remove(userId); + return; + } + customGestures.entrySet().removeIf(entry -> filter.matches(entry.getValue())); + if (customGestures.isEmpty()) { + mCustomInputGestures.remove(userId); + } } } @NonNull - public List<InputGestureData> getCustomInputGestures(int userId) { + public List<InputGestureData> getCustomInputGestures(int userId, + @Nullable InputGestureData.Filter filter) { synchronized (mGestureLock) { if (!mCustomInputGestures.contains(userId)) { return List.of(); } - return new ArrayList<>(mCustomInputGestures.get(userId).values()); + Map<InputGestureData.Trigger, InputGestureData> customGestures = + mCustomInputGestures.get(userId); + if (filter == null) { + return new ArrayList<>(customGestures.values()); + } + List<InputGestureData> result = new ArrayList<>(); + for (InputGestureData customGesture : customGestures.values()) { + if (filter.matches(customGesture)) { + result.add(customGesture); + } + } + return result; } } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index e0f3a9bd427a..f4dd71706761 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -3017,15 +3017,16 @@ public class InputManagerService extends IInputManager.Stub @Override @PermissionManuallyEnforced - public void removeAllCustomInputGestures(@UserIdInt int userId) { + public void removeAllCustomInputGestures(@UserIdInt int userId, int tag) { enforceManageKeyGesturePermission(); - mKeyGestureController.removeAllCustomInputGestures(userId); + mKeyGestureController.removeAllCustomInputGestures(userId, InputGestureData.Filter.of(tag)); } @Override - public AidlInputGestureData[] getCustomInputGestures(@UserIdInt int userId) { - return mKeyGestureController.getCustomInputGestures(userId); + public AidlInputGestureData[] getCustomInputGestures(@UserIdInt int userId, int tag) { + return mKeyGestureController.getCustomInputGestures(userId, + InputGestureData.Filter.of(tag)); } @Override diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index fc106404049c..155ffe805519 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -1021,13 +1021,16 @@ final class KeyGestureController { } @BinderThread - public void removeAllCustomInputGestures(@UserIdInt int userId) { - mInputGestureManager.removeAllCustomInputGestures(userId); + public void removeAllCustomInputGestures(@UserIdInt int userId, + @Nullable InputGestureData.Filter filter) { + mInputGestureManager.removeAllCustomInputGestures(userId, filter); } @BinderThread - public AidlInputGestureData[] getCustomInputGestures(@UserIdInt int userId) { - List<InputGestureData> customGestures = mInputGestureManager.getCustomInputGestures(userId); + public AidlInputGestureData[] getCustomInputGestures(@UserIdInt int userId, + @Nullable InputGestureData.Filter filter) { + List<InputGestureData> customGestures = mInputGestureManager.getCustomInputGestures(userId, + filter); AidlInputGestureData[] result = new AidlInputGestureData[customGestures.size()]; for (int i = 0; i < customGestures.size(); i++) { result[i] = customGestures.get(i).getAidlData(); diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java index acc8f6634f5c..f611c57dab03 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java @@ -35,6 +35,7 @@ import android.hardware.contexthub.MessageDeliveryStatus; import android.hardware.location.ContextHubInfo; import android.hardware.location.ContextHubMessage; import android.hardware.location.ContextHubTransaction; +import android.hardware.location.HubInfo; import android.hardware.location.IContextHubCallback; import android.hardware.location.IContextHubClient; import android.hardware.location.IContextHubClientCallback; @@ -57,6 +58,7 @@ import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Pair; import android.util.proto.ProtoOutputStream; @@ -134,6 +136,9 @@ public class ContextHubService extends IContextHubService.Stub { private Map<Integer, ContextHubInfo> mContextHubIdToInfoMap; private List<String> mSupportedContextHubPerms; private List<ContextHubInfo> mContextHubInfoList; + + @Nullable private final HubInfoRegistry mHubInfoRegistry; + private final RemoteCallbackList<IContextHubCallback> mCallbacksList = new RemoteCallbackList<>(); @@ -309,10 +314,21 @@ public class ContextHubService extends IContextHubService.Stub { mContext = context; long startTimeNs = SystemClock.elapsedRealtimeNanos(); mContextHubWrapper = contextHubWrapper; + if (!initContextHubServiceState(startTimeNs)) { Log.e(TAG, "Failed to initialize the Context Hub Service"); + mHubInfoRegistry = null; return; } + + if (Flags.offloadApi()) { + mHubInfoRegistry = new HubInfoRegistry(mContextHubWrapper); + Log.i(TAG, "Enabling generic offload API"); + } else { + mHubInfoRegistry = null; + Log.i(TAG, "Disabling generic offload API"); + } + initDefaultClientMap(); initLocationSettingNotifications(); @@ -427,7 +443,7 @@ public class ContextHubService extends IContextHubService.Stub { Pair<List<ContextHubInfo>, List<String>> hubInfo; try { - hubInfo = mContextHubWrapper.getHubs(); + hubInfo = mContextHubWrapper.getContextHubs(); } catch (RemoteException e) { Log.e(TAG, "RemoteException while getting Context Hub info", e); hubInfo = new Pair<>(Collections.emptyList(), Collections.emptyList()); @@ -713,6 +729,16 @@ public class ContextHubService extends IContextHubService.Stub { return mContextHubInfoList; } + @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) + @Override + public List<HubInfo> getHubs() throws RemoteException { + super.getHubs_enforcePermission(); + if (mHubInfoRegistry == null) { + return Collections.emptyList(); + } + return mHubInfoRegistry.getHubs(); + } + /** * Creates an internal load transaction callback to be used for old API clients * @@ -1417,6 +1443,8 @@ public class ContextHubService extends IContextHubService.Stub { } } + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + pw = ipw; pw.println("Dumping ContextHub Service"); pw.println(""); @@ -1428,6 +1456,11 @@ public class ContextHubService extends IContextHubService.Stub { pw.println("Supported permissions: " + Arrays.toString(mSupportedContextHubPerms.toArray())); pw.println(""); + + if (mHubInfoRegistry != null) { + mHubInfoRegistry.dump(ipw); + } + pw.println("=================== NANOAPPS ===================="); // Dump nanoAppHash mNanoAppStateManager.foreachNanoAppInstanceInfo(pw::println); diff --git a/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java b/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java new file mode 100644 index 000000000000..68de9dbda2e1 --- /dev/null +++ b/services/core/java/com/android/server/location/contexthub/HubInfoRegistry.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.location.contexthub; + +import android.hardware.location.HubInfo; +import android.os.RemoteException; +import android.util.IndentingPrintWriter; +import android.util.Log; + +import java.util.Collections; +import java.util.List; + +class HubInfoRegistry { + private static final String TAG = "HubInfoRegistry"; + + private final IContextHubWrapper mContextHubWrapper; + + private final List<HubInfo> mHubsInfo; + + HubInfoRegistry(IContextHubWrapper contextHubWrapper) { + List<HubInfo> hubInfos; + mContextHubWrapper = contextHubWrapper; + try { + hubInfos = mContextHubWrapper.getHubs(); + } catch (RemoteException e) { + Log.e(TAG, "RemoteException while getting Hub info", e); + hubInfos = Collections.emptyList(); + } + mHubsInfo = hubInfos; + } + + /** Retrieve the list of hubs available. */ + List<HubInfo> getHubs() { + return mHubsInfo; + } + + void dump(IndentingPrintWriter ipw) { + ipw.println(TAG); + + ipw.increaseIndent(); + for (HubInfo hubInfo : mHubsInfo) { + ipw.println(hubInfo); + } + ipw.decreaseIndent(); + } +} diff --git a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java index 5e9277ac0faf..6656a6fe9eb4 100644 --- a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java +++ b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java @@ -30,9 +30,11 @@ import android.hardware.contexthub.V1_2.HubAppInfo; import android.hardware.contexthub.V1_2.IContexthubCallback; import android.hardware.location.ContextHubInfo; import android.hardware.location.ContextHubTransaction; +import android.hardware.location.HubInfo; import android.hardware.location.NanoAppBinary; import android.hardware.location.NanoAppMessage; import android.hardware.location.NanoAppState; +import android.hardware.location.VendorHubInfo; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -52,13 +54,14 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; /** * @hide */ public abstract class IContextHubWrapper { + private static final boolean DEBUG = false; private static final String TAG = "IContextHubWrapper"; /** @@ -217,10 +220,14 @@ public abstract class IContextHubWrapper { return proxy == null ? null : new ContextHubWrapperAidl(proxy); } - /** - * Calls the appropriate getHubs function depending on the HAL version. - */ - public abstract Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException; + /** Calls the appropriate getHubs function depending on the HAL version. */ + public abstract Pair<List<ContextHubInfo>, List<String>> getContextHubs() + throws RemoteException; + + /** Calls the appropriate getHubs function depending on the HAL version. */ + public List<HubInfo> getHubs() throws RemoteException { + return Collections.emptyList(); + } /** * @return True if this version of the Contexthub HAL supports Location setting notifications. @@ -556,7 +563,7 @@ public abstract class IContextHubWrapper { mIsTestModeEnabled.set(false); } - public Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException { + public Pair<List<ContextHubInfo>, List<String>> getContextHubs() throws RemoteException { android.hardware.contexthub.IContextHub hub = getHub(); if (hub == null) { return new Pair<List<ContextHubInfo>, List<String>>(new ArrayList<ContextHubInfo>(), @@ -574,6 +581,47 @@ public abstract class IContextHubWrapper { return new Pair(hubInfoList, new ArrayList<String>(supportedPermissions)); } + public List<HubInfo> getHubs() throws RemoteException { + android.hardware.contexthub.IContextHub hub = getHub(); + if (hub == null) { + return Collections.emptyList(); + } + + List<HubInfo> retVal = new ArrayList<>(); + final List<android.hardware.contexthub.HubInfo> halHubs = hub.getHubs(); + + for (android.hardware.contexthub.HubInfo halHub : halHubs) { + /* HAL -> API Type conversion */ + final HubInfo hubInfo; + switch (halHub.hubDetails.getTag()) { + case android.hardware.contexthub.HubInfo.HubDetails.contextHubInfo: + ContextHubInfo contextHubInfo = + new ContextHubInfo(halHub.hubDetails.getContextHubInfo()); + hubInfo = new HubInfo(halHub.hubId, contextHubInfo); + break; + case android.hardware.contexthub.HubInfo.HubDetails.vendorHubInfo: + VendorHubInfo vendorHubInfo = + new VendorHubInfo(halHub.hubDetails.getVendorHubInfo()); + hubInfo = new HubInfo(halHub.hubId, vendorHubInfo); + break; + default: + Log.w(TAG, "getHubs: invalid hub: " + halHub); + // Invalid + continue; + } + + if (DEBUG) { + Log.i(TAG, "getHubs: hubInfo=" + hubInfo); + } + retVal.add(hubInfo); + } + + if (DEBUG) { + Log.i(TAG, "getHubs: total count=" + retVal.size()); + } + return retVal; + } + public boolean supportsLocationSettingNotifications() { return true; } @@ -1061,7 +1109,7 @@ public abstract class IContextHubWrapper { mHub = hub; } - public Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException { + public Pair<List<ContextHubInfo>, List<String>> getContextHubs() throws RemoteException { ArrayList<ContextHubInfo> hubInfoList = new ArrayList<>(); for (ContextHub hub : mHub.getHubs()) { hubInfoList.add(new ContextHubInfo(hub)); @@ -1106,7 +1154,7 @@ public abstract class IContextHubWrapper { mHub = hub; } - public Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException { + public Pair<List<ContextHubInfo>, List<String>> getContextHubs() throws RemoteException { ArrayList<ContextHubInfo> hubInfoList = new ArrayList<>(); for (ContextHub hub : mHub.getHubs()) { hubInfoList.add(new ContextHubInfo(hub)); @@ -1170,7 +1218,7 @@ public abstract class IContextHubWrapper { mHubInfo = new Pair(hubInfoList, supportedPermissions); } - public Pair<List<ContextHubInfo>, List<String>> getHubs() throws RemoteException { + public Pair<List<ContextHubInfo>, List<String>> getContextHubs() throws RemoteException { mHub.getHubs_1_2(this); return mHubInfo; } diff --git a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java index df44e50d2839..a92ac679b0f4 100644 --- a/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java +++ b/services/core/java/com/android/server/vibrator/ExternalVibrationSession.java @@ -45,6 +45,7 @@ final class ExternalVibrationSession extends Vibration void onExternalVibrationReleased(long vibrationId); } + private final long mSessionId = VibrationSession.nextSessionId(); private final ExternalVibration mExternalVibration; private final ExternalVibrationScale mScale = new ExternalVibrationScale(); private final VibratorManagerHooks mManagerHooks; @@ -65,6 +66,11 @@ final class ExternalVibrationSession extends Vibration } @Override + public long getSessionId() { + return mSessionId; + } + + @Override public long getCreateUptimeMillis() { return stats.getCreateUptimeMillis(); } @@ -148,7 +154,12 @@ final class ExternalVibrationSession extends Vibration @Override public void notifySyncedVibratorsCallback(long vibrationId) { - // ignored, external control does not expect callbacks from the vibrator manager + // ignored, external control does not expect callbacks from the vibrator manager for sync + } + + @Override + public void notifySessionCallback() { + // ignored, external control does not expect callbacks from the vibrator manager for session } boolean isHoldingSameVibration(ExternalVibration vib) { @@ -174,7 +185,8 @@ final class ExternalVibrationSession extends Vibration @Override public String toString() { return "ExternalVibrationSession{" - + "id=" + id + + "sessionId=" + mSessionId + + ", vibrationId=" + id + ", callerInfo=" + callerInfo + ", externalVibration=" + mExternalVibration + ", scale=" + mScale diff --git a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java index 67ba25f6b0b9..628221b09d77 100644 --- a/services/core/java/com/android/server/vibrator/SingleVibrationSession.java +++ b/services/core/java/com/android/server/vibrator/SingleVibrationSession.java @@ -35,6 +35,7 @@ final class SingleVibrationSession implements VibrationSession, IBinder.DeathRec private static final String TAG = "SingleVibrationSession"; private final Object mLock = new Object(); + private final long mSessionId = VibrationSession.nextSessionId(); private final IBinder mCallerToken; private final HalVibration mVibration; @@ -58,6 +59,11 @@ final class SingleVibrationSession implements VibrationSession, IBinder.DeathRec } @Override + public long getSessionId() { + return mSessionId; + } + + @Override public long getCreateUptimeMillis() { return mVibration.stats.getCreateUptimeMillis(); } @@ -155,9 +161,15 @@ final class SingleVibrationSession implements VibrationSession, IBinder.DeathRec } @Override + public void notifySessionCallback() { + // ignored, external control does not expect callbacks from the vibrator manager for session + } + + @Override public String toString() { return "SingleVibrationSession{" - + "callerToken= " + mCallerToken + + "sessionId= " + mSessionId + + ", callerToken= " + mCallerToken + ", vibration=" + mVibration + '}'; } diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java new file mode 100644 index 000000000000..07478e360d27 --- /dev/null +++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.vibrator; + +import static com.android.server.vibrator.VibrationSession.DebugInfo.formatTime; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.media.AudioAttributes; +import android.os.CancellationSignal; +import android.os.CombinedVibration; +import android.os.ExternalVibration; +import android.os.Handler; +import android.os.IBinder; +import android.os.ICancellationSignal; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.VibrationAttributes; +import android.os.vibrator.IVibrationSession; +import android.os.vibrator.IVibrationSessionCallback; +import android.util.IndentingPrintWriter; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.annotations.GuardedBy; + +import java.util.Arrays; +import java.util.Locale; +import java.util.NoSuchElementException; + +/** + * A vibration session started by a vendor request that can trigger {@link CombinedVibration}. + */ +final class VendorVibrationSession extends IVibrationSession.Stub + implements VibrationSession, CancellationSignal.OnCancelListener, IBinder.DeathRecipient { + private static final String TAG = "VendorVibrationSession"; + + /** Calls into VibratorManager functionality needed for playing an {@link ExternalVibration}. */ + interface VibratorManagerHooks { + + /** Tells the manager to end the vibration session. */ + void endSession(long sessionId, boolean shouldAbort); + + /** + * Tells the manager that the vibration session is finished and the vibrators can now be + * used for another vibration. + */ + void onSessionReleased(long sessionId); + } + + private final Object mLock = new Object(); + private final long mSessionId = VibrationSession.nextSessionId(); + private final ICancellationSignal mCancellationSignal = CancellationSignal.createTransport(); + private final int[] mVibratorIds; + private final long mCreateUptime; + private final long mCreateTime; // for debugging + private final IVibrationSessionCallback mCallback; + private final CallerInfo mCallerInfo; + private final VibratorManagerHooks mManagerHooks; + private final Handler mHandler; + + @GuardedBy("mLock") + private Status mStatus = Status.RUNNING; + @GuardedBy("mLock") + private Status mEndStatusRequest; + @GuardedBy("mLock") + private long mStartTime; // for debugging + @GuardedBy("mLock") + private long mEndUptime; + @GuardedBy("mLock") + private long mEndTime; // for debugging + + VendorVibrationSession(@NonNull CallerInfo callerInfo, @NonNull Handler handler, + @NonNull VibratorManagerHooks managerHooks, @NonNull int[] vibratorIds, + @NonNull IVibrationSessionCallback callback) { + mCreateUptime = SystemClock.uptimeMillis(); + mCreateTime = System.currentTimeMillis(); + mVibratorIds = vibratorIds; + mHandler = handler; + mCallback = callback; + mCallerInfo = callerInfo; + mManagerHooks = managerHooks; + CancellationSignal.fromTransport(mCancellationSignal).setOnCancelListener(this); + } + + @Override + public void vibrate(CombinedVibration vibration, String reason) { + // TODO(b/345414356): implement vibration support + throw new UnsupportedOperationException("Vendor session vibrations not yet implemented"); + } + + @Override + public void finishSession() { + // Do not abort session in HAL, wait for ongoing vibration requests to complete. + // This might take a while to end the session, but it can be aborted by cancelSession. + requestEndSession(Status.FINISHED, /* shouldAbort= */ false); + } + + @Override + public void cancelSession() { + // Always abort session in HAL while cancelling it. + // This might be triggered after finishSession was already called. + requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true); + } + + @Override + public long getSessionId() { + return mSessionId; + } + + @Override + public long getCreateUptimeMillis() { + return mCreateUptime; + } + + @Override + public boolean isRepeating() { + return false; + } + + @Override + public CallerInfo getCallerInfo() { + return mCallerInfo; + } + + @Override + public IBinder getCallerToken() { + return mCallback.asBinder(); + } + + @Override + public DebugInfo getDebugInfo() { + synchronized (mLock) { + return new DebugInfoImpl(mStatus, mCallerInfo, mCreateUptime, mCreateTime, mStartTime, + mEndUptime, mEndTime); + } + } + + @Override + public boolean wasEndRequested() { + synchronized (mLock) { + return mEndStatusRequest != null; + } + } + + @Override + public void onCancel() { + Slog.d(TAG, "Cancellation signal received, cancelling vibration session..."); + requestEnd(Status.CANCELLED_BY_USER, /* endedBy= */ null, /* immediate= */ false); + } + + @Override + public void binderDied() { + Slog.d(TAG, "Binder died, cancelling vibration session..."); + requestEnd(Status.CANCELLED_BINDER_DIED, /* endedBy= */ null, /* immediate= */ false); + } + + @Override + public boolean linkToDeath() { + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + Slog.e(TAG, "Error linking session to token death", e); + return false; + } + return true; + } + + @Override + public void unlinkToDeath() { + try { + mCallback.asBinder().unlinkToDeath(this, 0); + } catch (NoSuchElementException e) { + Slog.wtf(TAG, "Failed to unlink session to token death", e); + } + } + + @Override + public void requestEnd(@NonNull Status status, @Nullable CallerInfo endedBy, + boolean immediate) { + // All requests to end a session should abort it to stop ongoing vibrations, even if + // immediate flag is false. Only the #finishSession API will not abort and wait for + // session vibrations to complete, which might take a long time. + requestEndSession(status, /* shouldAbort= */ true); + } + + @Override + public void notifyVibratorCallback(int vibratorId, long vibrationId) { + // TODO(b/345414356): implement vibration support + } + + @Override + public void notifySyncedVibratorsCallback(long vibrationId) { + // TODO(b/345414356): implement vibration support + } + + @Override + public void notifySessionCallback() { + synchronized (mLock) { + // If end was not requested then the HAL has cancelled the session. + maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON); + maybeSetStatusToRequestedLocked(); + } + mManagerHooks.onSessionReleased(mSessionId); + } + + @Override + public String toString() { + synchronized (mLock) { + return "createTime: " + formatTime(mCreateTime, /*includeDate=*/ true) + + ", startTime: " + (mStartTime == 0 ? null : formatTime(mStartTime, + /* includeDate= */ true)) + + ", endTime: " + (mEndTime == 0 ? null : formatTime(mEndTime, + /* includeDate= */ true)) + + ", status: " + mStatus.name().toLowerCase(Locale.ROOT) + + ", callerInfo: " + mCallerInfo + + ", vibratorIds: " + Arrays.toString(mVibratorIds); + } + } + + public Status getStatus() { + synchronized (mLock) { + return mStatus; + } + } + + public boolean isStarted() { + synchronized (mLock) { + return mStartTime > 0; + } + } + + public boolean isEnded() { + synchronized (mLock) { + return mStatus != Status.RUNNING; + } + } + + public int[] getVibratorIds() { + return mVibratorIds; + } + + public ICancellationSignal getCancellationSignal() { + return mCancellationSignal; + } + + public void notifyStart() { + boolean isAlreadyEnded = false; + synchronized (mLock) { + if (isEnded()) { + // Session already ended, skip start callbacks. + isAlreadyEnded = true; + } else { + mStartTime = System.currentTimeMillis(); + // Run client callback in separate thread. + mHandler.post(() -> { + try { + mCallback.onStarted(this); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying vendor session started", e); + } + }); + } + } + if (isAlreadyEnded) { + // Session already ended, make sure we end it in the HAL. + mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true); + } + } + + private void requestEndSession(Status status, boolean shouldAbort) { + boolean shouldTriggerSessionHook = false; + synchronized (mLock) { + maybeSetEndRequestLocked(status); + if (isStarted()) { + // Always trigger session hook after it has started, in case new request aborts an + // already finishing session. Wait for HAL callback before actually ending here. + shouldTriggerSessionHook = true; + } else { + // Session did not start in the HAL, end it right away. + maybeSetStatusToRequestedLocked(); + } + } + if (shouldTriggerSessionHook) { + mManagerHooks.endSession(mSessionId, shouldAbort); + } + } + + @GuardedBy("mLock") + private void maybeSetEndRequestLocked(Status status) { + if (mEndStatusRequest != null) { + // End already requested, keep first requested status and time. + return; + } + mEndStatusRequest = status; + mEndTime = System.currentTimeMillis(); + mEndUptime = SystemClock.uptimeMillis(); + if (isStarted()) { + // Only trigger "finishing" callback if session started. + // Run client callback in separate thread. + mHandler.post(() -> { + try { + mCallback.onFinishing(); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying vendor session is finishing", e); + } + }); + } + } + + @GuardedBy("mLock") + private void maybeSetStatusToRequestedLocked() { + if (isEnded()) { + // End already set, keep first requested status and time. + return; + } + if (mEndStatusRequest == null) { + // No end status was requested, nothing to set. + return; + } + mStatus = mEndStatusRequest; + // Run client callback in separate thread. + final Status endStatus = mStatus; + mHandler.post(() -> { + try { + mCallback.onFinished(toSessionStatus(endStatus)); + } catch (RemoteException e) { + Slog.e(TAG, "Error notifying vendor session is finishing", e); + } + }); + } + + @android.os.vibrator.VendorVibrationSession.Status + private static int toSessionStatus(Status status) { + // Exhaustive switch to cover all possible internal status. + return switch (status) { + case FINISHED + -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS; + case IGNORED_UNSUPPORTED + -> STATUS_UNSUPPORTED; + case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER, + CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF, + CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON + -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED; + case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING, + IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE, + IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED, + IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER + -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED; + case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING, IGNORED_ERROR_SCHEDULING, + IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES, FINISHED_UNEXPECTED, RUNNING + -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR; + }; + } + + /** + * Holds lightweight debug information about the session that could potentially be kept in + * memory for a long time for bugreport dumpsys operations. + * + * Since DebugInfo can be kept in memory for a long time, it shouldn't hold any references to + * potentially expensive or resource-linked objects, such as {@link IBinder}. + */ + static final class DebugInfoImpl implements VibrationSession.DebugInfo { + private final Status mStatus; + private final CallerInfo mCallerInfo; + + private final long mCreateUptime; + private final long mCreateTime; + private final long mStartTime; + private final long mEndTime; + private final long mDurationMs; + + DebugInfoImpl(Status status, CallerInfo callerInfo, long createUptime, long createTime, + long startTime, long endUptime, long endTime) { + mStatus = status; + mCallerInfo = callerInfo; + mCreateUptime = createUptime; + mCreateTime = createTime; + mStartTime = startTime; + mEndTime = endTime; + mDurationMs = endUptime > 0 ? endUptime - createUptime : -1; + } + + @Override + public Status getStatus() { + return mStatus; + } + + @Override + public long getCreateUptimeMillis() { + return mCreateUptime; + } + + @Override + public CallerInfo getCallerInfo() { + return mCallerInfo; + } + + @Nullable + @Override + public Object getDumpAggregationKey() { + return null; // No aggregation. + } + + @Override + public void logMetrics(VibratorFrameworkStatsLogger statsLogger) { + } + + @Override + public void dump(ProtoOutputStream proto, long fieldId) { + final long token = proto.start(fieldId); + proto.write(VibrationProto.END_TIME, mEndTime); + proto.write(VibrationProto.DURATION_MS, mDurationMs); + proto.write(VibrationProto.STATUS, mStatus.ordinal()); + + final long attrsToken = proto.start(VibrationProto.ATTRIBUTES); + final VibrationAttributes attrs = mCallerInfo.attrs; + proto.write(VibrationAttributesProto.USAGE, attrs.getUsage()); + proto.write(VibrationAttributesProto.AUDIO_USAGE, attrs.getAudioUsage()); + proto.write(VibrationAttributesProto.FLAGS, attrs.getFlags()); + proto.end(attrsToken); + + proto.end(token); + } + + @Override + public void dump(IndentingPrintWriter pw) { + pw.println("VibrationSession:"); + pw.increaseIndent(); + pw.println("status = " + mStatus.name().toLowerCase(Locale.ROOT)); + pw.println("durationMs = " + mDurationMs); + pw.println("createTime = " + formatTime(mCreateTime, /*includeDate=*/ true)); + pw.println("startTime = " + formatTime(mStartTime, /*includeDate=*/ true)); + pw.println("endTime = " + (mEndTime == 0 ? null + : formatTime(mEndTime, /*includeDate=*/ true))); + pw.println("callerInfo = " + mCallerInfo); + pw.decreaseIndent(); + } + + @Override + public void dumpCompact(IndentingPrintWriter pw) { + // Follow pattern from Vibration.DebugInfoImpl for better debugging from dumpsys. + String timingsStr = String.format(Locale.ROOT, + "%s | %8s | %20s | duration: %5dms | start: %12s | end: %12s", + formatTime(mCreateTime, /*includeDate=*/ true), + "session", + mStatus.name().toLowerCase(Locale.ROOT), + mDurationMs, + mStartTime == 0 ? "" : formatTime(mStartTime, /*includeDate=*/ false), + mEndTime == 0 ? "" : formatTime(mEndTime, /*includeDate=*/ false)); + String paramStr = String.format(Locale.ROOT, + " | flags: %4s | usage: %s", + Long.toBinaryString(mCallerInfo.attrs.getFlags()), + mCallerInfo.attrs.usageToString()); + // Optional, most vibrations should not be defined via AudioAttributes + // so skip them to simplify the logs + String audioUsageStr = + mCallerInfo.attrs.getOriginalAudioUsage() != AudioAttributes.USAGE_UNKNOWN + ? " | audioUsage=" + AudioAttributes.usageToString( + mCallerInfo.attrs.getOriginalAudioUsage()) + : ""; + String callerStr = String.format(Locale.ROOT, + " | %s (uid=%d, deviceId=%d) | reason: %s", + mCallerInfo.opPkg, mCallerInfo.uid, mCallerInfo.deviceId, mCallerInfo.reason); + pw.println(timingsStr + paramStr + audioUsageStr + callerStr); + } + + @Override + public String toString() { + return "createTime: " + formatTime(mCreateTime, /* includeDate= */ true) + + ", startTime: " + formatTime(mStartTime, /* includeDate= */ true) + + ", endTime: " + (mEndTime == 0 ? null : formatTime(mEndTime, + /* includeDate= */ true)) + + ", durationMs: " + mDurationMs + + ", status: " + mStatus.name().toLowerCase(Locale.ROOT) + + ", callerInfo: " + mCallerInfo; + } + } +} diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java index bb2a17c698ee..27f92b2080e6 100644 --- a/services/core/java/com/android/server/vibrator/Vibration.java +++ b/services/core/java/com/android/server/vibrator/Vibration.java @@ -16,6 +16,8 @@ package com.android.server.vibrator; +import static com.android.server.vibrator.VibrationSession.DebugInfo.formatTime; + import android.annotation.NonNull; import android.annotation.Nullable; import android.media.AudioAttributes; @@ -31,9 +33,6 @@ import android.os.vibrator.VibrationEffectSegment; import android.util.IndentingPrintWriter; import android.util.proto.ProtoOutputStream; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; @@ -42,11 +41,6 @@ import java.util.concurrent.atomic.AtomicInteger; * The base class for all vibrations. */ abstract class Vibration { - private static final DateTimeFormatter DEBUG_TIME_FORMATTER = DateTimeFormatter.ofPattern( - "HH:mm:ss.SSS"); - private static final DateTimeFormatter DEBUG_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( - "MM-dd HH:mm:ss.SSS"); - // Used to generate globally unique vibration ids. private static final AtomicInteger sNextVibrationId = new AtomicInteger(1); // 0 = no callback @@ -399,12 +393,5 @@ abstract class Vibration { proto.write(PrimitiveSegmentProto.DELAY, segment.getDelay()); proto.end(token); } - - private String formatTime(long timeInMillis, boolean includeDate) { - return (includeDate ? DEBUG_DATE_TIME_FORMATTER : DEBUG_TIME_FORMATTER) - // Ensure timezone is retrieved at formatting time - .withZone(ZoneId.systemDefault()) - .format(Instant.ofEpochMilli(timeInMillis)); - } } } diff --git a/services/core/java/com/android/server/vibrator/VibrationSession.java b/services/core/java/com/android/server/vibrator/VibrationSession.java index b511ba8be405..ae95a70e2a4f 100644 --- a/services/core/java/com/android/server/vibrator/VibrationSession.java +++ b/services/core/java/com/android/server/vibrator/VibrationSession.java @@ -25,7 +25,11 @@ import android.util.IndentingPrintWriter; import android.util.proto.ProtoOutputStream; import java.io.PrintWriter; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; /** * Represents a generic vibration session that plays one or more vibration requests. @@ -39,6 +43,16 @@ import java.util.Objects; */ interface VibrationSession { + // Used to generate globally unique session ids. + AtomicInteger sNextSessionId = new AtomicInteger(1); // 0 = no callback + + static long nextSessionId() { + return sNextSessionId.getAndIncrement(); + } + + /** Returns the session id. */ + long getSessionId(); + /** Returns the session creation time from {@link android.os.SystemClock#uptimeMillis()}. */ long getCreateUptimeMillis(); @@ -105,6 +119,14 @@ interface VibrationSession { void notifySyncedVibratorsCallback(long vibrationId); /** + * Notify vibrator manager have completed the vibration session. + * + * <p>This will be called by the vibrator manager hardware callback indicating the session + * is complete, either because it was ended or cancelled by the service or the vendor. + */ + void notifySessionCallback(); + + /** * Session status with reference to values from vibratormanagerservice.proto for logging. */ enum Status { @@ -212,6 +234,17 @@ interface VibrationSession { */ interface DebugInfo { + DateTimeFormatter DEBUG_TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + DateTimeFormatter DEBUG_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( + "MM-dd HH:mm:ss.SSS"); + + static String formatTime(long timeInMillis, boolean includeDate) { + return (includeDate ? DEBUG_DATE_TIME_FORMATTER : DEBUG_TIME_FORMATTER) + // Ensure timezone is retrieved at formatting time + .withZone(ZoneId.systemDefault()) + .format(Instant.ofEpochMilli(timeInMillis)); + } + /** Return the vibration session status. */ Status getStatus(); diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index ff3491182a5f..476448148e28 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -32,6 +32,7 @@ import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.res.Resources; import android.hardware.vibrator.IVibrator; +import android.hardware.vibrator.IVibratorManager; import android.os.BatteryStats; import android.os.Binder; import android.os.Build; @@ -40,6 +41,7 @@ import android.os.ExternalVibration; import android.os.ExternalVibrationScale; import android.os.Handler; import android.os.IBinder; +import android.os.ICancellationSignal; import android.os.IExternalVibratorService; import android.os.IVibratorManagerService; import android.os.IVibratorStateListener; @@ -57,6 +59,7 @@ import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.Flags; +import android.os.vibrator.IVibrationSessionCallback; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.VibrationConfig; import android.os.vibrator.VibrationEffectSegment; @@ -103,7 +106,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private static final String EXTERNAL_VIBRATOR_SERVICE = "external_vibrator_service"; private static final String VIBRATOR_CONTROL_SERVICE = "android.frameworks.vibrator.IVibratorControlService/default"; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; private static final VibrationAttributes DEFAULT_ATTRIBUTES = new VibrationAttributes.Builder().build(); private static final int ATTRIBUTES_ALL_BYPASS_FLAGS = @@ -159,12 +162,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { new VibrationThreadCallbacks(); private final ExternalVibrationCallbacks mExternalVibrationCallbacks = new ExternalVibrationCallbacks(); + private final VendorVibrationSessionCallbacks mVendorVibrationSessionCallbacks = + new VendorVibrationSessionCallbacks(); @GuardedBy("mLock") private final SparseArray<AlwaysOnVibration> mAlwaysOnEffects = new SparseArray<>(); @GuardedBy("mLock") - private VibrationSession mCurrentVibration; + private VibrationSession mCurrentSession; @GuardedBy("mLock") - private VibrationSession mNextVibration; + private VibrationSession mNextSession; @GuardedBy("mLock") private boolean mServiceReady; @@ -191,14 +196,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // When the system is entering a non-interactive state, we want to cancel // vibrations in case a misbehaving app has abandoned them. synchronized (mLock) { - maybeClearCurrentAndNextVibrationsLocked( + maybeClearCurrentAndNextSessionsLocked( VibratorManagerService.this::shouldCancelOnScreenOffLocked, Status.CANCELLED_BY_SCREEN_OFF); } } else if (android.multiuser.Flags.addUiForSoundsFromBackgroundUsers() && intent.getAction().equals(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND)) { synchronized (mLock) { - maybeClearCurrentAndNextVibrationsLocked( + maybeClearCurrentAndNextSessionsLocked( VibratorManagerService.this::shouldCancelOnFgUserRequest, Status.CANCELLED_BY_FOREGROUND_USER); } @@ -215,14 +220,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { return; } synchronized (mLock) { - maybeClearCurrentAndNextVibrationsLocked( + maybeClearCurrentAndNextSessionsLocked( VibratorManagerService.this::shouldCancelAppOpModeChangedLocked, Status.CANCELLED_BY_APP_OPS); } } }; - static native long nativeInit(OnSyncedVibrationCompleteListener listener); + static native long nativeInit(VibratorManagerNativeCallbacks listener); static native long nativeGetFinalizer(); @@ -236,6 +241,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { static native void nativeCancelSynced(long nativeServicePtr); + static native boolean nativeStartSession(long nativeServicePtr, long sessionId, + int[] vibratorIds); + + static native void nativeEndSession(long nativeServicePtr, long sessionId, boolean shouldAbort); + + static native void nativeClearSessions(long nativeServicePtr); + @VisibleForTesting VibratorManagerService(Context context, Injector injector) { mContext = context; @@ -303,6 +315,9 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Reset the hardware to a default state, in case this is a runtime restart instead of a // fresh boot. mNativeWrapper.cancelSynced(); + if (Flags.vendorVibrationEffects()) { + mNativeWrapper.clearSessions(); + } for (int i = 0; i < mVibrators.size(); i++) { mVibrators.valueAt(i).reset(); } @@ -363,6 +378,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override // Binder call + public int getCapabilities() { + return (int) mCapabilities; + } + + @Override // Binder call @Nullable public VibratorInfo getVibratorInfo(int vibratorId) { final VibratorController controller = mVibrators.get(vibratorId); @@ -590,11 +610,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_ERROR_TOKEN); return null; } - if (effect.hasVendorEffects() - && !hasPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)) { - Slog.e(TAG, "vibrate; no permission for vendor effects"); - logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_MISSING_PERMISSION); - return null; + if (effect.hasVendorEffects()) { + if (!Flags.vendorVibrationEffects()) { + Slog.e(TAG, "vibrate; vendor effects feature disabled"); + logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_UNSUPPORTED); + return null; + } + if (!hasPermission(android.Manifest.permission.VIBRATE_VENDOR_EFFECTS)) { + Slog.e(TAG, "vibrate; no permission for vendor effects"); + logAndRecordVibrationAttempt(effect, callerInfo, Status.IGNORED_MISSING_PERMISSION); + return null; + } } enforceUpdateAppOpsStatsPermission(uid); if (!isEffectValid(effect)) { @@ -623,7 +649,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Check if ongoing vibration is more important than this vibration. if (ignoreStatus == null) { - Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoingLocked(session); + Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoingLocked(session); if (vibrationEndInfo != null) { ignoreStatus = vibrationEndInfo.status; ignoredBy = vibrationEndInfo.endedBy; @@ -634,8 +660,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (ignoreStatus == null) { final long ident = Binder.clearCallingIdentity(); try { - if (mCurrentVibration != null) { - if (shouldPipelineVibrationLocked(mCurrentVibration, vib)) { + if (mCurrentSession != null) { + if (shouldPipelineVibrationLocked(mCurrentSession, vib)) { // Don't cancel the current vibration if it's pipeline-able. // Note that if there is a pending next vibration that can't be // pipelined, it will have already cancelled the current one, so we @@ -645,12 +671,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } else { vib.stats.reportInterruptedAnotherVibration( - mCurrentVibration.getCallerInfo()); - mCurrentVibration.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo, + mCurrentSession.getCallerInfo()); + mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo, /* immediate= */ false); } } - clearNextVibrationLocked(Status.CANCELLED_SUPERSEDED, callerInfo); + clearNextSessionLocked(Status.CANCELLED_SUPERSEDED, callerInfo); ignoreStatus = startVibrationLocked(session); } finally { Binder.restoreCallingIdentity(ident); @@ -659,7 +685,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // Ignored or failed to start the vibration, end it and report metrics right away. if (ignoreStatus != null) { - endVibrationLocked(session, ignoreStatus, ignoredBy); + endSessionLocked(session, ignoreStatus, ignoredBy); } return vib; } @@ -681,19 +707,154 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { try { // TODO(b/370948466): investigate why token not checked on external vibrations. IBinder cancelToken = - (mNextVibration instanceof ExternalVibrationSession) ? null : token; - if (shouldCancelVibration(mNextVibration, usageFilter, cancelToken)) { - clearNextVibrationLocked(Status.CANCELLED_BY_USER); + (mNextSession instanceof ExternalVibrationSession) ? null : token; + if (shouldCancelSession(mNextSession, usageFilter, cancelToken)) { + clearNextSessionLocked(Status.CANCELLED_BY_USER); } cancelToken = - (mCurrentVibration instanceof ExternalVibrationSession) ? null : token; - if (shouldCancelVibration(mCurrentVibration, usageFilter, cancelToken)) { - mCurrentVibration.requestEnd(Status.CANCELLED_BY_USER); + (mCurrentSession instanceof ExternalVibrationSession) ? null : token; + if (shouldCancelSession(mCurrentSession, usageFilter, cancelToken)) { + mCurrentSession.requestEnd(Status.CANCELLED_BY_USER); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } finally { + Trace.traceEnd(TRACE_TAG_VIBRATOR); + } + } + + @android.annotation.EnforcePermission(allOf = { + android.Manifest.permission.VIBRATE, + android.Manifest.permission.VIBRATE_VENDOR_EFFECTS, + android.Manifest.permission.START_VIBRATION_SESSIONS, + }) + @Override // Binder call + public ICancellationSignal startVendorVibrationSession(int uid, int deviceId, String opPkg, + int[] vibratorIds, VibrationAttributes attrs, String reason, + IVibrationSessionCallback callback) { + startVendorVibrationSession_enforcePermission(); + Trace.traceBegin(TRACE_TAG_VIBRATOR, "startVibrationSession"); + try { + VendorVibrationSession session = startVendorVibrationSessionInternal( + uid, deviceId, opPkg, vibratorIds, attrs, reason, callback); + return session == null ? null : session.getCancellationSignal(); + } finally { + Trace.traceEnd(TRACE_TAG_VIBRATOR); + } + } + + VendorVibrationSession startVendorVibrationSessionInternal(int uid, int deviceId, String opPkg, + int[] vibratorIds, VibrationAttributes attrs, String reason, + IVibrationSessionCallback callback) { + if (!Flags.vendorVibrationEffects()) { + throw new UnsupportedOperationException("Vibration sessions not supported"); + } + attrs = fixupVibrationAttributes(attrs, /* effect= */ null); + CallerInfo callerInfo = new CallerInfo(attrs, uid, deviceId, opPkg, reason); + if (callback == null) { + Slog.e(TAG, "session callback must not be null"); + logAndRecordSessionAttempt(callerInfo, Status.IGNORED_ERROR_TOKEN); + return null; + } + if (vibratorIds == null) { + vibratorIds = new int[0]; + } + enforceUpdateAppOpsStatsPermission(uid); + VendorVibrationSession session = new VendorVibrationSession(callerInfo, mHandler, + mVendorVibrationSessionCallbacks, vibratorIds, callback); + + 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(); + } + + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "Starting session " + session.getSessionId()); + } + + Status ignoreStatus = null; + CallerInfo ignoredBy = null; + + // Check if HAL has capability to start sessions. + if ((mCapabilities & IVibratorManager.CAP_START_SESSIONS) == 0) { + if (DEBUG) { + Slog.d(TAG, "Missing capability to start sessions, ignoring request"); + } + ignoreStatus = Status.IGNORED_UNSUPPORTED; + } + + // Check if any vibrator ID was requested. + if (ignoreStatus == null && vibratorIds.length == 0) { + if (DEBUG) { + Slog.d(TAG, "Empty vibrator ids to start session, ignoring request"); + } + ignoreStatus = Status.IGNORED_UNSUPPORTED; + } + + // Check if user settings or DnD is set to ignore this session. + if (ignoreStatus == null) { + ignoreStatus = shouldIgnoreVibrationLocked(callerInfo); + } + + // Check if ongoing vibration is more important than this session. + if (ignoreStatus == null) { + Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoingLocked(session); + if (vibrationEndInfo != null) { + ignoreStatus = vibrationEndInfo.status; + ignoredBy = vibrationEndInfo.endedBy; + } + } + + if (ignoreStatus == null) { + final long ident = Binder.clearCallingIdentity(); + try { + // If not ignored so far then stop ongoing sessions before starting this one. + clearNextSessionLocked(Status.CANCELLED_SUPERSEDED, callerInfo); + if (mCurrentSession != null) { + mNextSession = session; + mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED, callerInfo, + /* immediate= */ false); + } else { + ignoreStatus = startVendorSessionLocked(session); } } finally { Binder.restoreCallingIdentity(ident); } } + + // Ignored or failed to start the session, end it and report metrics right away. + if (ignoreStatus != null) { + endSessionLocked(session, ignoreStatus, ignoredBy); + } + return session; + } + } + + @GuardedBy("mLock") + @Nullable + private Status startVendorSessionLocked(VendorVibrationSession session) { + Trace.traceBegin(TRACE_TAG_VIBRATOR, "startSessionLocked"); + try { + if (session.isEnded()) { + // Session already ended, possibly cancelled by app cancellation signal. + return session.getStatus(); + } + if (!session.linkToDeath()) { + return Status.IGNORED_ERROR_TOKEN; + } + if (!mNativeWrapper.startSession(session.getSessionId(), session.getVibratorIds())) { + Slog.e(TAG, "Error starting session " + session.getSessionId() + + " on vibrators " + Arrays.toString(session.getVibratorIds())); + session.unlinkToDeath(); + return Status.IGNORED_UNSUPPORTED; + } + session.notifyStart(); + mCurrentSession = session; + return null; } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); } @@ -747,8 +908,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println("CurrentVibration:"); pw.increaseIndent(); - if (mCurrentVibration != null) { - mCurrentVibration.getDebugInfo().dump(pw); + if (mCurrentSession != null) { + mCurrentSession.getDebugInfo().dump(pw); } else { pw.println("null"); } @@ -757,8 +918,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { pw.println("NextVibration:"); pw.increaseIndent(); - if (mNextVibration != null) { - mNextVibration.getDebugInfo().dump(pw); + if (mNextSession != null) { + mNextSession.getDebugInfo().dump(pw); } else { pw.println("null"); } @@ -782,8 +943,8 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { synchronized (mLock) { mVibrationSettings.dump(proto); mVibrationScaler.dump(proto); - if (mCurrentVibration != null) { - mCurrentVibration.getDebugInfo().dump(proto, + if (mCurrentSession != null) { + mCurrentSession.getDebugInfo().dump(proto, VibratorManagerServiceDumpProto.CURRENT_VIBRATION); } for (int i = 0; i < mVibrators.size(); i++) { @@ -816,18 +977,18 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } // TODO(b/372241975): investigate why external vibrations were not handled here before - if (mCurrentVibration == null - || (mCurrentVibration instanceof ExternalVibrationSession)) { + if (mCurrentSession == null + || (mCurrentSession instanceof ExternalVibrationSession)) { return; } - Status ignoreStatus = shouldIgnoreVibrationLocked(mCurrentVibration.getCallerInfo()); + Status ignoreStatus = shouldIgnoreVibrationLocked(mCurrentSession.getCallerInfo()); if (inputDevicesChanged || (ignoreStatus != null)) { if (DEBUG) { Slog.d(TAG, "Canceling vibration because settings changed: " + (inputDevicesChanged ? "input devices changed" : ignoreStatus)); } - mCurrentVibration.requestEnd(Status.CANCELLED_BY_SETTINGS_UPDATE); + mCurrentSession.requestEnd(Status.CANCELLED_BY_SETTINGS_UPDATE); } } } @@ -866,15 +1027,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (mInputDeviceDelegate.isAvailable()) { return startVibrationOnInputDevicesLocked(session.getVibration()); } - if (mCurrentVibration == null) { + if (mCurrentSession == null) { return startVibrationOnThreadLocked(session); } // If there's already a vibration queued (waiting for the previous one to finish // cancelling), end it cleanly and replace it with the new one. // Note that we don't consider pipelining here, because new pipelined ones should // replace pending non-executing pipelined ones anyway. - clearNextVibrationLocked(Status.IGNORED_SUPERSEDED, session.getCallerInfo()); - mNextVibration = session; + clearNextSessionLocked(Status.IGNORED_SUPERSEDED, session.getCallerInfo()); + mNextSession = session; return null; } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); @@ -891,16 +1052,16 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { case AppOpsManager.MODE_ALLOWED: Trace.asyncTraceBegin(TRACE_TAG_VIBRATOR, "vibration", 0); // Make sure mCurrentVibration is set while triggering the VibrationThread. - mCurrentVibration = session; - if (!mCurrentVibration.linkToDeath()) { + mCurrentSession = session; + if (!mCurrentSession.linkToDeath()) { // Shouldn't happen. The method call already logs. - mCurrentVibration = null; // Aborted. + mCurrentSession = null; // Aborted. return Status.IGNORED_ERROR_TOKEN; } if (!mVibrationThread.runVibrationOnVibrationThread(conductor)) { // Shouldn't happen. The method call already logs. session.setVibrationConductor(null); // Rejected by thread, clear it in session. - mCurrentVibration = null; // Aborted. + mCurrentSession = null; // Aborted. return Status.IGNORED_ERROR_SCHEDULING; } return null; @@ -914,23 +1075,29 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @GuardedBy("mLock") - private void maybeStartNextSingleVibrationLocked() { - if (mNextVibration instanceof SingleVibrationSession session) { - mNextVibration = null; + private void maybeStartNextSessionLocked() { + if (mNextSession instanceof SingleVibrationSession session) { + mNextSession = null; Status errorStatus = startVibrationOnThreadLocked(session); if (errorStatus != null) { - endVibrationLocked(session, errorStatus); + endSessionLocked(session, errorStatus); } - } + } else if (mNextSession instanceof VendorVibrationSession session) { + mNextSession = null; + Status errorStatus = startVendorSessionLocked(session); + if (errorStatus != null) { + endSessionLocked(session, errorStatus); + } + } // External vibrations cannot be started asynchronously. } @GuardedBy("mLock") - private void endVibrationLocked(VibrationSession session, Status status) { - endVibrationLocked(session, status, /* endedBy= */ null); + private void endSessionLocked(VibrationSession session, Status status) { + endSessionLocked(session, status, /* endedBy= */ null); } @GuardedBy("mLock") - private void endVibrationLocked(VibrationSession session, Status status, CallerInfo endedBy) { + private void endSessionLocked(VibrationSession session, Status status, CallerInfo endedBy) { session.requestEnd(status, endedBy, /* immediate= */ false); logAndRecordVibration(session.getDebugInfo()); } @@ -975,6 +1142,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { VibrationScaler.ADAPTIVE_SCALE_NONE)); } + private void logAndRecordSessionAttempt(CallerInfo callerInfo, Status status) { + logAndRecordVibration( + new VendorVibrationSession.DebugInfoImpl(status, callerInfo, + SystemClock.uptimeMillis(), System.currentTimeMillis(), + /* startTime= */ 0, /* endUptime= */ 0, /* endTime= */ 0)); + } + private void logAndRecordVibration(DebugInfo info) { info.logMetrics(mFrameworkStatsLogger); logVibrationStatus(info.getCallerInfo().uid, info.getCallerInfo().attrs, info.getStatus()); @@ -1026,25 +1200,40 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } + private void onVibrationSessionComplete(long sessionId) { + synchronized (mLock) { + if (mCurrentSession == null || mCurrentSession.getSessionId() != sessionId) { + if (DEBUG) { + Slog.d(TAG, "Vibration session " + sessionId + " callback ignored"); + } + return; + } + if (DEBUG) { + Slog.d(TAG, "Vibration session " + sessionId + " complete, notifying session"); + } + mCurrentSession.notifySessionCallback(); + } + } + private void onSyncedVibrationComplete(long vibrationId) { synchronized (mLock) { - if (mCurrentVibration != null) { + if (mCurrentSession != null) { if (DEBUG) { Slog.d(TAG, "Synced vibration " + vibrationId + " complete, notifying thread"); } - mCurrentVibration.notifySyncedVibratorsCallback(vibrationId); + mCurrentSession.notifySyncedVibratorsCallback(vibrationId); } } } private void onVibrationComplete(int vibratorId, long vibrationId) { synchronized (mLock) { - if (mCurrentVibration != null) { + if (mCurrentSession != null) { if (DEBUG) { Slog.d(TAG, "Vibration " + vibrationId + " on vibrator " + vibratorId + " complete, notifying thread"); } - mCurrentVibration.notifyVibratorCallback(vibratorId, vibrationId); + mCurrentSession.notifyVibratorCallback(vibratorId, vibrationId); } } } @@ -1056,10 +1245,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { */ @GuardedBy("mLock") @Nullable - private Vibration.EndInfo shouldIgnoreVibrationForOngoingLocked(VibrationSession session) { - if (mNextVibration != null) { - Vibration.EndInfo vibrationEndInfo = shouldIgnoreVibrationForOngoing(session, - mNextVibration); + private Vibration.EndInfo shouldIgnoreForOngoingLocked(VibrationSession session) { + if (mNextSession != null) { + Vibration.EndInfo vibrationEndInfo = shouldIgnoreForOngoing(session, + mNextSession); if (vibrationEndInfo != null) { // Next vibration has higher importance than the new one, so the new vibration // should be ignored. @@ -1067,13 +1256,13 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } - if (mCurrentVibration != null) { - if (mCurrentVibration.wasEndRequested()) { + if (mCurrentSession != null) { + if (mCurrentSession.wasEndRequested()) { // Current session has ended or is cancelling, should not block incoming vibrations. return null; } - return shouldIgnoreVibrationForOngoing(session, mCurrentVibration); + return shouldIgnoreForOngoing(session, mCurrentSession); } return null; @@ -1086,7 +1275,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { * @return a Vibration.EndInfo if the vibration should be ignored, null otherwise. */ @Nullable - private static Vibration.EndInfo shouldIgnoreVibrationForOngoing( + private static Vibration.EndInfo shouldIgnoreForOngoing( @NonNull VibrationSession newSession, @NonNull VibrationSession ongoingSession) { int newSessionImportance = getVibrationImportance(newSession); @@ -1214,11 +1403,15 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { * @param tokenFilter The binder token to identify the vibration origin. Only vibrations * started with the same token can be cancelled with it. */ - private boolean shouldCancelVibration(@Nullable VibrationSession session, int usageFilter, + private boolean shouldCancelSession(@Nullable VibrationSession session, int usageFilter, @Nullable IBinder tokenFilter) { if (session == null) { return false; } + if (session instanceof VendorVibrationSession) { + // Vendor sessions should not be cancelled by Vibrator.cancel API. + return false; + } if ((tokenFilter != null) && (tokenFilter != session.getCallerToken())) { // Vibration from a different app, this should not cancel it. return false; @@ -1572,10 +1765,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVibrationThreadReleased"); try { synchronized (mLock) { - if (!(mCurrentVibration instanceof SingleVibrationSession session)) { + if (!(mCurrentSession instanceof SingleVibrationSession session)) { if (Build.IS_DEBUGGABLE) { Slog.wtf(TAG, "VibrationSession invalid on vibration thread release." - + " currentSession=" + mCurrentVibration); + + " currentSession=" + mCurrentSession); } // Only single vibration sessions are ended by thread being released. Abort. return; @@ -1586,11 +1779,11 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { + " expected=%d, released=%d", session.getVibration().id, vibrationId)); } - finishAppOpModeLocked(mCurrentVibration.getCallerInfo()); - clearCurrentVibrationLocked(); + finishAppOpModeLocked(mCurrentSession.getCallerInfo()); + clearCurrentSessionLocked(); Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0); - // Start next vibration if it's a single vibration waiting for the thread. - maybeStartNextSingleVibrationLocked(); + // Start next vibration if it's waiting for the thread. + maybeStartNextSessionLocked(); } } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); @@ -1613,10 +1806,10 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationReleased"); try { synchronized (mLock) { - if (!(mCurrentVibration instanceof ExternalVibrationSession session)) { + if (!(mCurrentSession instanceof ExternalVibrationSession session)) { if (Build.IS_DEBUGGABLE) { Slog.wtf(TAG, "VibrationSession invalid on external vibration release." - + " currentSession=" + mCurrentVibration); + + " currentSession=" + mCurrentSession); } // Only external vibration sessions are ended by this callback. Abort. return; @@ -1627,10 +1820,62 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { + " expected=%d, released=%d", session.id, vibrationId)); } setExternalControl(false, session.stats); - clearCurrentVibrationLocked(); - // Start next vibration if it's a single vibration waiting for the external - // control to be over. - maybeStartNextSingleVibrationLocked(); + clearCurrentSessionLocked(); + // Start next vibration if it's waiting for the external control to be over. + maybeStartNextSessionLocked(); + } + } finally { + Trace.traceEnd(TRACE_TAG_VIBRATOR); + } + } + } + + /** + * Implementation of {@link ExternalVibrationSession.VibratorManagerHooks} that controls + * external vibrations and reports them when finished. + */ + private final class VendorVibrationSessionCallbacks + implements VendorVibrationSession.VibratorManagerHooks { + + @Override + public void endSession(long sessionId, boolean shouldAbort) { + if (DEBUG) { + Slog.d(TAG, "Vibration session " + sessionId + + (shouldAbort ? " aborting" : " ending")); + } + Trace.traceBegin(TRACE_TAG_VIBRATOR, "endSession"); + try { + mNativeWrapper.endSession(sessionId, shouldAbort); + } finally { + Trace.traceEnd(TRACE_TAG_VIBRATOR); + } + } + + @Override + public void onSessionReleased(long sessionId) { + if (DEBUG) { + Slog.d(TAG, "Vibration session " + sessionId + " released"); + } + Trace.traceBegin(TRACE_TAG_VIBRATOR, "onVendorSessionReleased"); + try { + synchronized (mLock) { + if (!(mCurrentSession instanceof VendorVibrationSession session)) { + if (Build.IS_DEBUGGABLE) { + Slog.wtf(TAG, "VibrationSession invalid on vibration session release." + + " currentSession=" + mCurrentSession); + } + // Only vendor vibration sessions are ended by this callback. Abort. + return; + } + if (Build.IS_DEBUGGABLE && (session.getSessionId() != sessionId)) { + Slog.wtf(TAG, TextUtils.formatSimple( + "SessionId mismatch on vendor vibration session release." + + " expected=%d, released=%d", + session.getSessionId(), sessionId)); + } + clearCurrentSessionLocked(); + // Start next vibration if it's waiting for the HAL session to be over. + maybeStartNextSessionLocked(); } } finally { Trace.traceEnd(TRACE_TAG_VIBRATOR); @@ -1638,19 +1883,22 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } } - /** Listener for synced vibration completion callbacks from native. */ + /** Listener for vibrator manager completion callbacks from native. */ @VisibleForTesting - interface OnSyncedVibrationCompleteListener { + interface VibratorManagerNativeCallbacks { /** Callback triggered when synced vibration is complete. */ - void onComplete(long vibrationId); + void onSyncedVibrationComplete(long vibrationId); + + /** Callback triggered when vibration session is complete. */ + void onVibrationSessionComplete(long sessionId); } /** * Implementation of listeners to native vibrators with a weak reference to this service. */ private static final class VibrationCompleteListener implements - VibratorController.OnVibrationCompleteListener, OnSyncedVibrationCompleteListener { + VibratorController.OnVibrationCompleteListener, VibratorManagerNativeCallbacks { private WeakReference<VibratorManagerService> mServiceRef; VibrationCompleteListener(VibratorManagerService service) { @@ -1658,7 +1906,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override - public void onComplete(long vibrationId) { + public void onSyncedVibrationComplete(long vibrationId) { VibratorManagerService service = mServiceRef.get(); if (service != null) { service.onSyncedVibrationComplete(vibrationId); @@ -1666,6 +1914,14 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } @Override + public void onVibrationSessionComplete(long sessionId) { + VibratorManagerService service = mServiceRef.get(); + if (service != null) { + service.onVibrationSessionComplete(sessionId); + } + } + + @Override public void onComplete(int vibratorId, long vibrationId) { VibratorManagerService service = mServiceRef.get(); if (service != null) { @@ -1698,7 +1954,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { private long mNativeServicePtr = 0; /** Returns native pointer to newly created controller and connects with HAL service. */ - public void init(OnSyncedVibrationCompleteListener listener) { + public void init(VibratorManagerNativeCallbacks listener) { mNativeServicePtr = nativeInit(listener); long finalizerPtr = nativeGetFinalizer(); @@ -1734,6 +1990,21 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { public void cancelSynced() { nativeCancelSynced(mNativeServicePtr); } + + /** Start vibration session. */ + public boolean startSession(long sessionId, @NonNull int[] vibratorIds) { + return nativeStartSession(mNativeServicePtr, sessionId, vibratorIds); + } + + /** End vibration session. */ + public void endSession(long sessionId, boolean shouldAbort) { + nativeEndSession(mNativeServicePtr, sessionId, shouldAbort); + } + + /** Clear vibration sessions. */ + public void clearSessions() { + nativeClearSessions(mNativeServicePtr); + } } /** Keep records of vibrations played and provide debug information for this service. */ @@ -1853,46 +2124,46 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { /** Clears mNextVibration if set, ending it cleanly */ @GuardedBy("mLock") - private void clearNextVibrationLocked(Status status) { - clearNextVibrationLocked(status, /* endedBy= */ null); + private void clearNextSessionLocked(Status status) { + clearNextSessionLocked(status, /* endedBy= */ null); } /** Clears mNextVibration if set, ending it cleanly */ @GuardedBy("mLock") - private void clearNextVibrationLocked(Status status, CallerInfo endedBy) { - if (mNextVibration != null) { + private void clearNextSessionLocked(Status status, CallerInfo endedBy) { + if (mNextSession != null) { if (DEBUG) { - Slog.d(TAG, "Dropping pending vibration from " + mNextVibration.getCallerInfo() + Slog.d(TAG, "Dropping pending vibration from " + mNextSession.getCallerInfo() + " with status: " + status); } // Clearing next vibration before playing it, end it and report metrics right away. - endVibrationLocked(mNextVibration, status, endedBy); - mNextVibration = null; + endSessionLocked(mNextSession, status, endedBy); + mNextSession = null; } } /** Clears mCurrentVibration if set, reporting metrics */ @GuardedBy("mLock") - private void clearCurrentVibrationLocked() { - if (mCurrentVibration != null) { - mCurrentVibration.unlinkToDeath(); - logAndRecordVibration(mCurrentVibration.getDebugInfo()); - mCurrentVibration = null; + private void clearCurrentSessionLocked() { + if (mCurrentSession != null) { + mCurrentSession.unlinkToDeath(); + logAndRecordVibration(mCurrentSession.getDebugInfo()); + mCurrentSession = null; mLock.notify(); // Notify if waiting for current vibration to end. } } @GuardedBy("mLock") - private void maybeClearCurrentAndNextVibrationsLocked( + private void maybeClearCurrentAndNextSessionsLocked( Predicate<VibrationSession> shouldEndSessionPredicate, Status endStatus) { // TODO(b/372241975): investigate why external vibrations were not handled here before - if (!(mNextVibration instanceof ExternalVibrationSession) - && shouldEndSessionPredicate.test(mNextVibration)) { - clearNextVibrationLocked(endStatus); + if (!(mNextSession instanceof ExternalVibrationSession) + && shouldEndSessionPredicate.test(mNextSession)) { + clearNextSessionLocked(endStatus); } - if (!(mCurrentVibration instanceof ExternalVibrationSession) - && shouldEndSessionPredicate.test(mCurrentVibration)) { - mCurrentVibration.requestEnd(endStatus); + if (!(mCurrentSession instanceof ExternalVibrationSession) + && shouldEndSessionPredicate.test(mCurrentSession)) { + mCurrentSession.requestEnd(endStatus); } } @@ -1902,12 +2173,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { * * @return true if the vibration completed, or false if waiting timed out. */ - public boolean waitForCurrentVibrationEnd(long maxWaitMillis) { + public boolean waitForCurrentSessionEnd(long maxWaitMillis) { long now = SystemClock.elapsedRealtime(); long deadline = now + maxWaitMillis; synchronized (mLock) { while (true) { - if (mCurrentVibration == null) { + if (mCurrentSession == null) { return true; // Done } if (now >= deadline) { // Note that thread.wait(0) waits indefinitely. @@ -1965,7 +2236,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { synchronized (mLock) { if (!hasExternalControlCapability()) { - endVibrationLocked(session, Status.IGNORED_UNSUPPORTED); + endSessionLocked(session, Status.IGNORED_UNSUPPORTED); return session.getScale(); } @@ -1976,17 +2247,17 @@ 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(session, Status.IGNORED_MISSING_PERMISSION); + endSessionLocked(session, Status.IGNORED_MISSING_PERMISSION); return session.getScale(); } Status ignoreStatus = shouldIgnoreVibrationLocked(session.callerInfo); if (ignoreStatus != null) { - endVibrationLocked(session, ignoreStatus); + endSessionLocked(session, ignoreStatus); return session.getScale(); } - if ((mCurrentVibration instanceof ExternalVibrationSession evs) + if ((mCurrentSession instanceof ExternalVibrationSession evs) && evs.isHoldingSameVibration(vib)) { // We are already playing this external vibration, so we can return the same // scale calculated in the previous call to this method. @@ -1994,17 +2265,17 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { } // Check if ongoing vibration is more important than this vibration. - Vibration.EndInfo ignoreInfo = shouldIgnoreVibrationForOngoingLocked(session); + Vibration.EndInfo ignoreInfo = shouldIgnoreForOngoingLocked(session); if (ignoreInfo != null) { - endVibrationLocked(session, ignoreInfo.status, ignoreInfo.endedBy); + endSessionLocked(session, ignoreInfo.status, ignoreInfo.endedBy); return session.getScale(); } // First clear next request, so it won't start when the current one ends. - clearNextVibrationLocked(Status.IGNORED_FOR_EXTERNAL, session.callerInfo); - mNextVibration = session; + clearNextSessionLocked(Status.IGNORED_FOR_EXTERNAL, session.callerInfo); + mNextSession = session; - if (mCurrentVibration != null) { + if (mCurrentSession != null) { // Cancel any vibration that may be playing and ready the vibrator, even if // we have an externally controlled vibration playing already. // Since the interface defines that only one externally controlled @@ -2016,36 +2287,36 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // as we would need to mute the old one still if it came from a different // controller. session.stats.reportInterruptedAnotherVibration( - mCurrentVibration.getCallerInfo()); - mCurrentVibration.requestEnd(Status.CANCELLED_SUPERSEDED, + mCurrentSession.getCallerInfo()); + mCurrentSession.requestEnd(Status.CANCELLED_SUPERSEDED, session.callerInfo, /* immediate= */ true); waitForCompletion = true; } } // Wait for lock and interact with HAL to set external control outside main lock. if (waitForCompletion) { - if (!waitForCurrentVibrationEnd(VIBRATION_CANCEL_WAIT_MILLIS)) { + if (!waitForCurrentSessionEnd(VIBRATION_CANCEL_WAIT_MILLIS)) { Slog.e(TAG, "Timed out waiting for vibration to cancel"); synchronized (mLock) { - if (mNextVibration == session) { - mNextVibration = null; + if (mNextSession == session) { + mNextSession = null; } - endVibrationLocked(session, Status.IGNORED_ERROR_CANCELLING); + endSessionLocked(session, Status.IGNORED_ERROR_CANCELLING); return session.getScale(); } } } synchronized (mLock) { - if (mNextVibration == session) { + if (mNextSession == session) { // This is still the next vibration to be played. - mNextVibration = null; + mNextSession = null; } else { // A new request took the place of this one, maybe with higher importance. // Next vibration already cleared with the right status, just return here. return session.getScale(); } if (!session.linkToDeath()) { - endVibrationLocked(session, Status.IGNORED_ERROR_TOKEN); + endSessionLocked(session, Status.IGNORED_ERROR_TOKEN); return session.getScale(); } if (DEBUG) { @@ -2062,7 +2333,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { // should be ignored or scaled. mVibrationSettings.update(); } - mCurrentVibration = session; + mCurrentSession = session; session.scale(mVibrationScaler, attrs.getUsage()); // Vibrator will start receiving data from external channels after this point. @@ -2080,12 +2351,12 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { Trace.traceBegin(TRACE_TAG_VIBRATOR, "onExternalVibrationStop"); try { synchronized (mLock) { - if ((mCurrentVibration instanceof ExternalVibrationSession evs) + if ((mCurrentSession instanceof ExternalVibrationSession evs) && evs.isHoldingSameVibration(vib)) { if (DEBUG) { Slog.d(TAG, "Stopping external vibration: " + vib); } - mCurrentVibration.requestEnd(Status.FINISHED); + mCurrentSession.requestEnd(Status.FINISHED); } } } finally { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 5cff37a36656..10f096c9031b 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -858,10 +858,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo, wallpaper.getDescription()); } else { + WallpaperDescription desc = new WallpaperDescription.Builder().setComponent( + (connection.mInfo != null) ? connection.mInfo.getComponent() + : null).build(); connection.mService.attach(connection, mToken, TYPE_WALLPAPER, false, wpdData.mWidth, wpdData.mHeight, - wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo, - /* description= */ null); + wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo, desc); } } catch (RemoteException e) { Slog.w(TAG, "Failed attaching wallpaper on display", e); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 73ae51c6e64a..14be59f27f84 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -254,6 +254,7 @@ import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; import static org.xmlpull.v1.XmlPullParser.END_TAG; import static org.xmlpull.v1.XmlPullParser.START_TAG; +import android.Manifest; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -10308,6 +10309,21 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (pictureInPictureArgs != null && pictureInPictureArgs.hasSourceBoundsHint()) { pictureInPictureArgs.getSourceRectHint().offset(windowBounds.left, windowBounds.top); } + + if (android.app.Flags.enableTvImplicitEnterPipRestriction()) { + PackageManager pm = mAtmService.mContext.getPackageManager(); + if (pictureInPictureArgs.isAutoEnterEnabled() + && pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + && pm.checkPermission(Manifest.permission.TV_IMPLICIT_ENTER_PIP, packageName) + == PackageManager.PERMISSION_DENIED) { + Log.i(TAG, + "Auto-enter PiP only allowed on TV if android.permission" + + ".TV_IMPLICIT_ENTER_PIP permission is held by the app."); + PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder(); + builder.setAutoEnterEnabled(false); + pictureInPictureArgs.copyOnlySet(builder.build()); + } + } } private void applyLocaleOverrideIfNeeded(Configuration resolvedConfig) { diff --git a/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp b/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp index a47ab9d27c17..46be79e7c097 100644 --- a/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp +++ b/services/core/jni/com_android_server_vibrator_VibratorManagerService.cpp @@ -16,27 +16,32 @@ #define LOG_TAG "VibratorManagerService" -#include <nativehelper/JNIHelp.h> -#include "android_runtime/AndroidRuntime.h" -#include "core_jni_helpers.h" -#include "jni.h" +#include "com_android_server_vibrator_VibratorManagerService.h" +#include <nativehelper/JNIHelp.h> #include <utils/Log.h> #include <utils/misc.h> - #include <vibratorservice/VibratorManagerHalController.h> -#include "com_android_server_vibrator_VibratorManagerService.h" +#include <unordered_map> + +#include "android_runtime/AndroidRuntime.h" +#include "core_jni_helpers.h" +#include "jni.h" namespace android { static JavaVM* sJvm = nullptr; -static jmethodID sMethodIdOnComplete; +static jmethodID sMethodIdOnSyncedVibrationComplete; +static jmethodID sMethodIdOnVibrationSessionComplete; static std::mutex gManagerMutex; static vibrator::ManagerHalController* gManager GUARDED_BY(gManagerMutex) = nullptr; class NativeVibratorManagerService { public: + using IVibrationSession = aidl::android::hardware::vibrator::IVibrationSession; + using VibrationSessionConfig = aidl::android::hardware::vibrator::VibrationSessionConfig; + NativeVibratorManagerService(JNIEnv* env, jobject callbackListener) : mHal(std::make_unique<vibrator::ManagerHalController>()), mCallbackListener(env->NewGlobalRef(callbackListener)) { @@ -52,15 +57,69 @@ public: vibrator::ManagerHalController* hal() const { return mHal.get(); } - std::function<void()> createCallback(jlong vibrationId) { + std::function<void()> createSyncedVibrationCallback(jlong vibrationId) { return [vibrationId, this]() { auto jniEnv = GetOrAttachJNIEnvironment(sJvm); - jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnComplete, vibrationId); + jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnSyncedVibrationComplete, + vibrationId); }; } + std::function<void()> createVibrationSessionCallback(jlong sessionId) { + return [sessionId, this]() { + auto jniEnv = GetOrAttachJNIEnvironment(sJvm); + jniEnv->CallVoidMethod(mCallbackListener, sMethodIdOnVibrationSessionComplete, + sessionId); + std::lock_guard<std::mutex> lock(mSessionMutex); + auto it = mSessions.find(sessionId); + if (it != mSessions.end()) { + mSessions.erase(it); + } + }; + } + + bool startSession(jlong sessionId, const std::vector<int32_t>& vibratorIds) { + VibrationSessionConfig config; + auto callback = createVibrationSessionCallback(sessionId); + auto result = hal()->startSession(vibratorIds, config, callback); + if (!result.isOk()) { + return false; + } + + std::lock_guard<std::mutex> lock(mSessionMutex); + mSessions[sessionId] = std::move(result.value()); + return true; + } + + void closeSession(jlong sessionId) { + std::lock_guard<std::mutex> lock(mSessionMutex); + auto it = mSessions.find(sessionId); + if (it != mSessions.end()) { + it->second->close(); + // Keep session, it can still be aborted. + } + } + + void abortSession(jlong sessionId) { + std::lock_guard<std::mutex> lock(mSessionMutex); + auto it = mSessions.find(sessionId); + if (it != mSessions.end()) { + it->second->abort(); + mSessions.erase(it); + } + } + + void clearSessions() { + hal()->clearSessions(); + std::lock_guard<std::mutex> lock(mSessionMutex); + mSessions.clear(); + } + private: + std::mutex mSessionMutex; const std::unique_ptr<vibrator::ManagerHalController> mHal; + std::unordered_map<jlong, std::shared_ptr<IVibrationSession>> mSessions + GUARDED_BY(mSessionMutex); const jobject mCallbackListener; }; @@ -142,7 +201,7 @@ static jboolean nativeTriggerSynced(JNIEnv* env, jclass /* clazz */, jlong servi ALOGE("nativeTriggerSynced failed because native service was not initialized"); return JNI_FALSE; } - auto callback = service->createCallback(vibrationId); + auto callback = service->createSyncedVibrationCallback(vibrationId); return service->hal()->triggerSynced(callback).isOk() ? JNI_TRUE : JNI_FALSE; } @@ -156,8 +215,47 @@ static void nativeCancelSynced(JNIEnv* env, jclass /* clazz */, jlong servicePtr service->hal()->cancelSynced(); } +static jboolean nativeStartSession(JNIEnv* env, jclass /* clazz */, jlong servicePtr, + jlong sessionId, jintArray vibratorIds) { + NativeVibratorManagerService* service = + reinterpret_cast<NativeVibratorManagerService*>(servicePtr); + if (service == nullptr) { + ALOGE("nativeStartSession failed because native service was not initialized"); + return JNI_FALSE; + } + jsize size = env->GetArrayLength(vibratorIds); + std::vector<int32_t> ids(size); + env->GetIntArrayRegion(vibratorIds, 0, size, reinterpret_cast<jint*>(ids.data())); + return service->startSession(sessionId, ids) ? JNI_TRUE : JNI_FALSE; +} + +static void nativeEndSession(JNIEnv* env, jclass /* clazz */, jlong servicePtr, jlong sessionId, + jboolean shouldAbort) { + NativeVibratorManagerService* service = + reinterpret_cast<NativeVibratorManagerService*>(servicePtr); + if (service == nullptr) { + ALOGE("nativeEndSession failed because native service was not initialized"); + return; + } + if (shouldAbort) { + service->abortSession(sessionId); + } else { + service->closeSession(sessionId); + } +} + +static void nativeClearSessions(JNIEnv* env, jclass /* clazz */, jlong servicePtr) { + NativeVibratorManagerService* service = + reinterpret_cast<NativeVibratorManagerService*>(servicePtr); + if (service == nullptr) { + ALOGE("nativeClearSessions failed because native service was not initialized"); + return; + } + service->clearSessions(); +} + inline static constexpr auto sNativeInitMethodSignature = - "(Lcom/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener;)J"; + "(Lcom/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks;)J"; static const JNINativeMethod method_table[] = { {"nativeInit", sNativeInitMethodSignature, (void*)nativeInit}, @@ -167,15 +265,20 @@ static const JNINativeMethod method_table[] = { {"nativePrepareSynced", "(J[I)Z", (void*)nativePrepareSynced}, {"nativeTriggerSynced", "(JJ)Z", (void*)nativeTriggerSynced}, {"nativeCancelSynced", "(J)V", (void*)nativeCancelSynced}, + {"nativeStartSession", "(JJ[I)Z", (void*)nativeStartSession}, + {"nativeEndSession", "(JJZ)V", (void*)nativeEndSession}, + {"nativeClearSessions", "(J)V", (void*)nativeClearSessions}, }; int register_android_server_vibrator_VibratorManagerService(JavaVM* jvm, JNIEnv* env) { sJvm = jvm; auto listenerClassName = - "com/android/server/vibrator/VibratorManagerService$OnSyncedVibrationCompleteListener"; + "com/android/server/vibrator/VibratorManagerService$VibratorManagerNativeCallbacks"; jclass listenerClass = FindClassOrDie(env, listenerClassName); - sMethodIdOnComplete = GetMethodIDOrDie(env, listenerClass, "onComplete", "(J)V"); - + sMethodIdOnSyncedVibrationComplete = + GetMethodIDOrDie(env, listenerClass, "onSyncedVibrationComplete", "(J)V"); + sMethodIdOnVibrationSessionComplete = + GetMethodIDOrDie(env, listenerClass, "onVibrationSessionComplete", "(J)V"); return jniRegisterNativeMethods(env, "com/android/server/vibrator/VibratorManagerService", method_table, NELEM(method_table)); } diff --git a/services/proguard.flags b/services/proguard.flags index cdd41abf6c7c..977bd19a7236 100644 --- a/services/proguard.flags +++ b/services/proguard.flags @@ -82,7 +82,7 @@ -keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbAlsaJackDetector { *; } -keep,allowoptimization,allowaccessmodification class com.android.server.usb.UsbAlsaMidiDevice { *; } -keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorController$OnVibrationCompleteListener { *; } --keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorManagerService$OnSyncedVibrationCompleteListener { *; } +-keep,allowoptimization,allowaccessmodification class com.android.server.vibrator.VibratorManagerService$VibratorManagerNativeCallbacks { *; } -keepclasseswithmembers,allowoptimization,allowaccessmodification class com.android.server.** { *** *FromNative(...); } diff --git a/services/tests/displayservicetests/AndroidManifest.xml b/services/tests/displayservicetests/AndroidManifest.xml index 37a34eeb9724..205ff058275a 100644 --- a/services/tests/displayservicetests/AndroidManifest.xml +++ b/services/tests/displayservicetests/AndroidManifest.xml @@ -29,7 +29,6 @@ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> <uses-permission android:name="android.permission.MANAGE_USB" /> - <uses-permission android:name="android.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE" /> <!-- Permissions needed for DisplayTransformManagerTest --> <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" /> diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index c741cae1c135..80e5ee39c13d 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -19,13 +19,16 @@ package com.android.server.display; import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY; import static android.Manifest.permission.ADD_TRUSTED_DISPLAY; import static android.Manifest.permission.CAPTURE_VIDEO_OUTPUT; +import static android.Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS; import static android.Manifest.permission.MANAGE_DISPLAYS; +import static android.Manifest.permission.MODIFY_USER_PREFERRED_DISPLAY_MODE; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED; +import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID; @@ -96,6 +99,7 @@ import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerGlobal; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayManagerInternal.DisplayOffloader; +import android.hardware.display.DisplayTopology; import android.hardware.display.DisplayViewport; import android.hardware.display.DisplayedContentSample; import android.hardware.display.DisplayedContentSamplingAttributes; @@ -111,11 +115,13 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.MessageQueue; +import android.os.PermissionEnforcer; import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.SystemProperties; import android.os.UserManager; +import android.os.test.FakePermissionEnforcer; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; @@ -251,6 +257,8 @@ public class DisplayManagerServiceTest { private int[] mAllowedHdrOutputTypes; + private final FakePermissionEnforcer mPermissionEnforcer = new FakePermissionEnforcer(); + private final DisplayManagerService.Injector mShortMockedInjector = new DisplayManagerService.Injector() { @Override @@ -428,6 +436,13 @@ public class DisplayManagerServiceTest { when(mContext.getResources()).thenReturn(mResources); mUserManager = Mockito.spy(mContext.getSystemService(UserManager.class)); + mPermissionEnforcer.grant(CONTROL_DISPLAY_BRIGHTNESS); + mPermissionEnforcer.grant(MODIFY_USER_PREFERRED_DISPLAY_MODE); + doReturn(Context.PERMISSION_ENFORCER_SERVICE).when(mContext).getSystemServiceName( + eq(PermissionEnforcer.class)); + doReturn(mPermissionEnforcer).when(mContext).getSystemService( + eq(Context.PERMISSION_ENFORCER_SERVICE)); + VirtualDeviceManager vdm = new VirtualDeviceManager(mIVirtualDeviceManager, mContext); when(mContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm); when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); @@ -3667,6 +3682,87 @@ public class DisplayManagerServiceTest { verify(mMockVirtualDisplayAdapter).releaseVirtualDisplayLocked(binder, callingUid); } + @Test + public void testGetDisplayTopology() { + Settings.Global.putInt(mContext.getContentResolver(), + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 1); + manageDisplaysPermission(/* granted= */ true); + when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true); + DisplayManagerService displayManager = + new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + DisplayManagerService.BinderService displayManagerBinderService = + displayManager.new BinderService(); + registerDefaultDisplays(displayManager); + initDisplayPowerController(localService); + + DisplayTopology topology = displayManagerBinderService.getDisplayTopology(); + assertNotNull(topology); + DisplayTopology.TreeNode display = topology.getRoot(); + assertNotNull(display); + assertEquals(Display.DEFAULT_DISPLAY, display.getDisplayId()); + } + + @Test + public void testGetDisplayTopology_NullIfFlagDisabled() { + manageDisplaysPermission(/* granted= */ true); + when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(false); + DisplayManagerService displayManager = + new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + DisplayManagerService.BinderService displayManagerBinderService = + displayManager.new BinderService(); + registerDefaultDisplays(displayManager); + initDisplayPowerController(localService); + + DisplayTopology topology = displayManagerBinderService.getDisplayTopology(); + assertNull(topology); + } + + @Test + public void testGetDisplayTopology_withoutPermission_shouldThrowException() { + when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true); + DisplayManagerService displayManager = + new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + DisplayManagerService.BinderService displayManagerBinderService = + displayManager.new BinderService(); + registerDefaultDisplays(displayManager); + initDisplayPowerController(localService); + + assertThrows(SecurityException.class, displayManagerBinderService::getDisplayTopology); + } + + @Test + public void testSetDisplayTopology() { + manageDisplaysPermission(/* granted= */ true); + when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true); + DisplayManagerService displayManager = + new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + DisplayManagerService.BinderService displayManagerBinderService = + displayManager.new BinderService(); + registerDefaultDisplays(displayManager); + initDisplayPowerController(localService); + + displayManagerBinderService.setDisplayTopology(new DisplayTopology()); + } + + @Test + public void testSetDisplayTopology_withoutPermission_shouldThrowException() { + when(mMockFlags.isDisplayTopologyEnabled()).thenReturn(true); + DisplayManagerService displayManager = + new DisplayManagerService(mContext, mBasicInjector); + DisplayManagerInternal localService = displayManager.new LocalService(); + DisplayManagerService.BinderService displayManagerBinderService = + displayManager.new BinderService(); + registerDefaultDisplays(displayManager); + initDisplayPowerController(localService); + + assertThrows(SecurityException.class, + () -> displayManagerBinderService.setDisplayTopology(new DisplayTopology())); + } + private void initDisplayPowerController(DisplayManagerInternal localService) { localService.initPowerManagement(new DisplayManagerInternal.DisplayPowerCallbacks() { @Override @@ -3850,6 +3946,10 @@ public class DisplayManagerServiceTest { DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo(); displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked()); displayDeviceInfo.modeId = modeId; + if (modeId > 0 && modeId <= displayDeviceInfo.supportedModes.length) { + displayDeviceInfo.renderFrameRate = + displayDeviceInfo.supportedModes[modeId - 1].getRefreshRate(); + } updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo); } @@ -4017,9 +4117,11 @@ public class DisplayManagerServiceTest { private void manageDisplaysPermission(boolean granted) { if (granted) { doNothing().when(mContext).enforceCallingOrSelfPermission(eq(MANAGE_DISPLAYS), any()); + mPermissionEnforcer.grant(MANAGE_DISPLAYS); } else { doThrow(new SecurityException("MANAGE_DISPLAYS permission denied")).when(mContext) .enforceCallingOrSelfPermission(eq(MANAGE_DISPLAYS), any()); + mPermissionEnforcer.revoke(MANAGE_DISPLAYS); } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 27fd1e6187bb..e64d9855bc54 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -139,6 +139,7 @@ public final class DisplayPowerControllerTest { private TestLooper mTestLooper; private Handler mHandler; private DisplayPowerControllerHolder mHolder; + private DisplayBrightnessState mDisplayBrightnessState; private Sensor mProxSensor; @Mock @@ -187,6 +188,7 @@ public final class DisplayPowerControllerTest { mClock = new OffsettableClock.Stopped(); mTestLooper = new TestLooper(mClock::now); mHandler = new Handler(mTestLooper.getLooper()); + mDisplayBrightnessState = DisplayBrightnessState.builder().build(); // Set some settings to minimize unexpected events and have a consistent starting state Settings.System.putInt(mContext.getContentResolver(), @@ -1453,10 +1455,11 @@ public final class DisplayPowerControllerTest { when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f); when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f); when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f); - when(mHolder.clamperController.clamp(any(), anyFloat(), anyBoolean(), anyInt())).thenAnswer( - invocation -> DisplayBrightnessState.builder() - .setIsSlowChange(invocation.getArgument(2)) - .setBrightness(invocation.getArgument(1)) + when(mHolder.clamperController.clamp(any(), any(), anyFloat(), + anyBoolean(), anyInt())).thenAnswer( + invocation -> DisplayBrightnessState.Builder.from(mDisplayBrightnessState) + .setIsSlowChange(invocation.getArgument(3)) + .setBrightness(invocation.getArgument(2)) .setMaxBrightness(PowerManager.BRIGHTNESS_MAX) .setCustomAnimationRate(transitionRate).build()); @@ -1477,10 +1480,11 @@ public final class DisplayPowerControllerTest { when(mHolder.displayPowerState.getColorFadeLevel()).thenReturn(1.0f); when(mHolder.displayPowerState.getScreenBrightness()).thenReturn(.2f); when(mHolder.displayPowerState.getSdrScreenBrightness()).thenReturn(.1f); - when(mHolder.clamperController.clamp(any(), anyFloat(), anyBoolean(), anyInt())).thenAnswer( - invocation -> DisplayBrightnessState.builder() - .setIsSlowChange(invocation.getArgument(2)) - .setBrightness(invocation.getArgument(1)) + when(mHolder.clamperController.clamp(any(), any(), anyFloat(), + anyBoolean(), anyInt())).thenAnswer( + invocation -> DisplayBrightnessState.Builder.from(mDisplayBrightnessState) + .setIsSlowChange(invocation.getArgument(3)) + .setBrightness(invocation.getArgument(2)) .setMaxBrightness(PowerManager.BRIGHTNESS_MAX) .setCustomAnimationRate(transitionRate).build()); @@ -2574,10 +2578,11 @@ public final class DisplayPowerControllerTest { BrightnessClamperController clamperController = mock(BrightnessClamperController.class); when(hbmController.getCurrentBrightnessMax()).thenReturn(PowerManager.BRIGHTNESS_MAX); - when(clamperController.clamp(any(), anyFloat(), anyBoolean(), anyInt())).thenAnswer( - invocation -> DisplayBrightnessState.builder() - .setIsSlowChange(invocation.getArgument(2)) - .setBrightness(invocation.getArgument(1)) + when(clamperController.clamp(any(), any(), anyFloat(), anyBoolean(), + anyInt())).thenAnswer( + invocation -> DisplayBrightnessState.Builder.from(mDisplayBrightnessState) + .setIsSlowChange(invocation.getArgument(3)) + .setBrightness(invocation.getArgument(2)) .setMaxBrightness(PowerManager.BRIGHTNESS_MAX) .setCustomAnimationRate(-1).build()); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt index 85e73561cf59..a2d2a81b20b4 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt @@ -16,6 +16,7 @@ package com.android.server.display +import android.hardware.display.DisplayTopology import android.util.DisplayMetrics import android.view.Display import android.view.DisplayInfo diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyTest.kt deleted file mode 100644 index cd8c26d0d337..000000000000 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyTest.kt +++ /dev/null @@ -1,476 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.display - -import android.view.Display -import com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_BOTTOM -import com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_TOP -import com.android.server.display.DisplayTopology.TreeNode.Position.POSITION_RIGHT -import com.google.common.truth.Truth.assertThat -import org.junit.Test - -class DisplayTopologyTest { - private val topology = DisplayTopology() - - @Test - fun addOneDisplay() { - val displayId = 1 - val width = 800f - val height = 600f - - topology.addDisplay(displayId, width, height) - - assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId) - - val display = topology.mRoot!! - assertThat(display.mDisplayId).isEqualTo(displayId) - assertThat(display.mWidth).isEqualTo(width) - assertThat(display.mHeight).isEqualTo(height) - assertThat(display.mChildren).isEmpty() - } - - @Test - fun addTwoDisplays() { - val displayId1 = 1 - val width1 = 800f - val height1 = 600f - - val displayId2 = 2 - val width2 = 1000f - val height2 = 1500f - - topology.addDisplay(displayId1, width1, height1) - topology.addDisplay(displayId2, width2, height2) - - assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1) - - val display1 = topology.mRoot!! - assertThat(display1.mDisplayId).isEqualTo(displayId1) - assertThat(display1.mWidth).isEqualTo(width1) - assertThat(display1.mHeight).isEqualTo(height1) - assertThat(display1.mChildren).hasSize(1) - - val display2 = display1.mChildren[0] - assertThat(display2.mDisplayId).isEqualTo(displayId2) - assertThat(display2.mWidth).isEqualTo(width2) - assertThat(display2.mHeight).isEqualTo(height2) - assertThat(display2.mChildren).isEmpty() - assertThat(display2.mPosition).isEqualTo(POSITION_TOP) - assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2) - } - - @Test - fun addManyDisplays() { - val displayId1 = 1 - val width1 = 800f - val height1 = 600f - - val displayId2 = 2 - val width2 = 1000f - val height2 = 1500f - - topology.addDisplay(displayId1, width1, height1) - topology.addDisplay(displayId2, width2, height2) - - val noOfDisplays = 30 - for (i in 3..noOfDisplays) { - topology.addDisplay(/* displayId= */ i, width1, height1) - } - - assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1) - - val display1 = topology.mRoot!! - assertThat(display1.mDisplayId).isEqualTo(displayId1) - assertThat(display1.mWidth).isEqualTo(width1) - assertThat(display1.mHeight).isEqualTo(height1) - assertThat(display1.mChildren).hasSize(1) - - val display2 = display1.mChildren[0] - assertThat(display2.mDisplayId).isEqualTo(displayId2) - assertThat(display2.mWidth).isEqualTo(width2) - assertThat(display2.mHeight).isEqualTo(height2) - assertThat(display2.mChildren).hasSize(1) - assertThat(display2.mPosition).isEqualTo(POSITION_TOP) - assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2) - - var display = display2 - for (i in 3..noOfDisplays) { - display = display.mChildren[0] - assertThat(display.mDisplayId).isEqualTo(i) - assertThat(display.mWidth).isEqualTo(width1) - assertThat(display.mHeight).isEqualTo(height1) - // The last display should have no children - assertThat(display.mChildren).hasSize(if (i < noOfDisplays) 1 else 0) - assertThat(display.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(display.mOffset).isEqualTo(0) - } - } - - @Test - fun removeDisplays() { - val displayId1 = 1 - val width1 = 800f - val height1 = 600f - - val displayId2 = 2 - val width2 = 1000f - val height2 = 1500f - - topology.addDisplay(displayId1, width1, height1) - topology.addDisplay(displayId2, width2, height2) - - val noOfDisplays = 30 - for (i in 3..noOfDisplays) { - topology.addDisplay(/* displayId= */ i, width1, height1) - } - - var removedDisplays = arrayOf(20) - topology.removeDisplay(20) - - assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1) - - var display1 = topology.mRoot!! - assertThat(display1.mDisplayId).isEqualTo(displayId1) - assertThat(display1.mWidth).isEqualTo(width1) - assertThat(display1.mHeight).isEqualTo(height1) - assertThat(display1.mChildren).hasSize(1) - - var display2 = display1.mChildren[0] - assertThat(display2.mDisplayId).isEqualTo(displayId2) - assertThat(display2.mWidth).isEqualTo(width2) - assertThat(display2.mHeight).isEqualTo(height2) - assertThat(display2.mChildren).hasSize(1) - assertThat(display2.mPosition).isEqualTo(POSITION_TOP) - assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2) - - var display = display2 - for (i in 3..noOfDisplays) { - if (i in removedDisplays) { - continue - } - display = display.mChildren[0] - assertThat(display.mDisplayId).isEqualTo(i) - assertThat(display.mWidth).isEqualTo(width1) - assertThat(display.mHeight).isEqualTo(height1) - // The last display should have no children - assertThat(display.mChildren).hasSize(if (i < noOfDisplays) 1 else 0) - assertThat(display.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(display.mOffset).isEqualTo(0) - } - - topology.removeDisplay(22) - removedDisplays += 22 - topology.removeDisplay(23) - removedDisplays += 23 - topology.removeDisplay(25) - removedDisplays += 25 - - assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1) - - display1 = topology.mRoot!! - assertThat(display1.mDisplayId).isEqualTo(displayId1) - assertThat(display1.mWidth).isEqualTo(width1) - assertThat(display1.mHeight).isEqualTo(height1) - assertThat(display1.mChildren).hasSize(1) - - display2 = display1.mChildren[0] - assertThat(display2.mDisplayId).isEqualTo(displayId2) - assertThat(display2.mWidth).isEqualTo(width2) - assertThat(display2.mHeight).isEqualTo(height2) - assertThat(display2.mChildren).hasSize(1) - assertThat(display2.mPosition).isEqualTo(POSITION_TOP) - assertThat(display2.mOffset).isEqualTo(width1 / 2 - width2 / 2) - - display = display2 - for (i in 3..noOfDisplays) { - if (i in removedDisplays) { - continue - } - display = display.mChildren[0] - assertThat(display.mDisplayId).isEqualTo(i) - assertThat(display.mWidth).isEqualTo(width1) - assertThat(display.mHeight).isEqualTo(height1) - // The last display should have no children - assertThat(display.mChildren).hasSize(if (i < noOfDisplays) 1 else 0) - assertThat(display.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(display.mOffset).isEqualTo(0) - } - } - - @Test - fun removeAllDisplays() { - val displayId = 1 - val width = 800f - val height = 600f - - topology.addDisplay(displayId, width, height) - topology.removeDisplay(displayId) - - assertThat(topology.mPrimaryDisplayId).isEqualTo(Display.INVALID_DISPLAY) - assertThat(topology.mRoot).isNull() - } - - @Test - fun removeDisplayThatDoesNotExist() { - val displayId = 1 - val width = 800f - val height = 600f - - topology.addDisplay(displayId, width, height) - topology.removeDisplay(3) - - assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId) - - val display = topology.mRoot!! - assertThat(display.mDisplayId).isEqualTo(displayId) - assertThat(display.mWidth).isEqualTo(width) - assertThat(display.mHeight).isEqualTo(height) - assertThat(display.mChildren).isEmpty() - } - - @Test - fun removePrimaryDisplay() { - val displayId1 = 1 - val displayId2 = 2 - val width = 800f - val height = 600f - - topology.addDisplay(displayId1, width, height) - topology.addDisplay(displayId2, width, height) - topology.mPrimaryDisplayId = displayId2 - topology.removeDisplay(displayId2) - - assertThat(topology.mPrimaryDisplayId).isEqualTo(displayId1) - val display = topology.mRoot!! - assertThat(display.mDisplayId).isEqualTo(displayId1) - assertThat(display.mWidth).isEqualTo(width) - assertThat(display.mHeight).isEqualTo(height) - assertThat(display.mChildren).isEmpty() - } - - @Test - fun normalization_noOverlaps_leavesTopologyUnchanged() { - val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, - /* height= */ 600f, /* position= */ null, /* offset= */ 0f) - topology.mRoot = display1 - - val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f, - /* height= */ 200f, POSITION_RIGHT, /* offset= */ 0f) - display1.mChildren.add(display2) - - val primaryDisplayId = 3 - val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f, - /* height= */ 200f, POSITION_RIGHT, /* offset= */ 400f) - display1.mChildren.add(display3) - topology.mPrimaryDisplayId = primaryDisplayId - - val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f, - /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) - display2.mChildren.add(display4) - - topology.normalize() - - assertThat(topology.mPrimaryDisplayId).isEqualTo(primaryDisplayId) - - val actualDisplay1 = topology.mRoot!! - assertThat(actualDisplay1.mDisplayId).isEqualTo(1) - assertThat(actualDisplay1.mWidth).isEqualTo(200f) - assertThat(actualDisplay1.mHeight).isEqualTo(600f) - assertThat(actualDisplay1.mChildren).hasSize(2) - - val actualDisplay2 = actualDisplay1.mChildren[0] - assertThat(actualDisplay2.mDisplayId).isEqualTo(2) - assertThat(actualDisplay2.mWidth).isEqualTo(600f) - assertThat(actualDisplay2.mHeight).isEqualTo(200f) - assertThat(actualDisplay2.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(actualDisplay2.mOffset).isEqualTo(0f) - assertThat(actualDisplay2.mChildren).hasSize(1) - - val actualDisplay3 = actualDisplay1.mChildren[1] - assertThat(actualDisplay3.mDisplayId).isEqualTo(3) - assertThat(actualDisplay3.mWidth).isEqualTo(600f) - assertThat(actualDisplay3.mHeight).isEqualTo(200f) - assertThat(actualDisplay3.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(actualDisplay3.mOffset).isEqualTo(400f) - assertThat(actualDisplay3.mChildren).isEmpty() - - val actualDisplay4 = actualDisplay2.mChildren[0] - assertThat(actualDisplay4.mDisplayId).isEqualTo(4) - assertThat(actualDisplay4.mWidth).isEqualTo(200f) - assertThat(actualDisplay4.mHeight).isEqualTo(600f) - assertThat(actualDisplay4.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(actualDisplay4.mOffset).isEqualTo(0f) - assertThat(actualDisplay4.mChildren).isEmpty() - } - - @Test - fun normalization_moveDisplayWithoutReparenting() { - val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, - /* height= */ 600f, /* position= */ null, /* offset= */ 0f) - topology.mRoot = display1 - - val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 200f, - /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) - display1.mChildren.add(display2) - - val primaryDisplayId = 3 - val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f, - /* height= */ 200f, POSITION_RIGHT, /* offset= */ 10f) - display1.mChildren.add(display3) - topology.mPrimaryDisplayId = primaryDisplayId - - val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f, - /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) - display2.mChildren.add(display4) - - // Display 3 becomes a child of display 2. Display 4 gets moved without changing its parent. - topology.normalize() - - assertThat(topology.mPrimaryDisplayId).isEqualTo(primaryDisplayId) - - val actualDisplay1 = topology.mRoot!! - assertThat(actualDisplay1.mDisplayId).isEqualTo(1) - assertThat(actualDisplay1.mWidth).isEqualTo(200f) - assertThat(actualDisplay1.mHeight).isEqualTo(600f) - assertThat(actualDisplay1.mChildren).hasSize(1) - - val actualDisplay2 = actualDisplay1.mChildren[0] - assertThat(actualDisplay2.mDisplayId).isEqualTo(2) - assertThat(actualDisplay2.mWidth).isEqualTo(200f) - assertThat(actualDisplay2.mHeight).isEqualTo(600f) - assertThat(actualDisplay2.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(actualDisplay2.mOffset).isEqualTo(0f) - assertThat(actualDisplay2.mChildren).hasSize(2) - - val actualDisplay3 = actualDisplay2.mChildren[1] - assertThat(actualDisplay3.mDisplayId).isEqualTo(3) - assertThat(actualDisplay3.mWidth).isEqualTo(600f) - assertThat(actualDisplay3.mHeight).isEqualTo(200f) - assertThat(actualDisplay3.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(actualDisplay3.mOffset).isEqualTo(10f) - assertThat(actualDisplay3.mChildren).isEmpty() - - val actualDisplay4 = actualDisplay2.mChildren[0] - assertThat(actualDisplay4.mDisplayId).isEqualTo(4) - assertThat(actualDisplay4.mWidth).isEqualTo(200f) - assertThat(actualDisplay4.mHeight).isEqualTo(600f) - assertThat(actualDisplay4.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(actualDisplay4.mOffset).isEqualTo(210f) - assertThat(actualDisplay4.mChildren).isEmpty() - } - - @Test - fun normalization_moveDisplayWithoutReparenting_offsetOutOfBounds() { - val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, - /* height= */ 50f, /* position= */ null, /* offset= */ 0f) - topology.mRoot = display1 - - val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f, - /* height= */ 200f, POSITION_RIGHT, /* offset= */ 0f) - display1.mChildren.add(display2) - - val primaryDisplayId = 3 - val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f, - /* height= */ 200f, POSITION_RIGHT, /* offset= */ 10f) - display1.mChildren.add(display3) - topology.mPrimaryDisplayId = primaryDisplayId - - // Display 3 gets moved and its left side is still on the same line as the right side - // of Display 1, but it no longer touches it (the offset is out of bounds), so Display 2 - // becomes its new parent. - topology.normalize() - - assertThat(topology.mPrimaryDisplayId).isEqualTo(primaryDisplayId) - - val actualDisplay1 = topology.mRoot!! - assertThat(actualDisplay1.mDisplayId).isEqualTo(1) - assertThat(actualDisplay1.mWidth).isEqualTo(200f) - assertThat(actualDisplay1.mHeight).isEqualTo(50f) - assertThat(actualDisplay1.mChildren).hasSize(1) - - val actualDisplay2 = actualDisplay1.mChildren[0] - assertThat(actualDisplay2.mDisplayId).isEqualTo(2) - assertThat(actualDisplay2.mWidth).isEqualTo(600f) - assertThat(actualDisplay2.mHeight).isEqualTo(200f) - assertThat(actualDisplay2.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(actualDisplay2.mOffset).isEqualTo(0f) - assertThat(actualDisplay2.mChildren).hasSize(1) - - val actualDisplay3 = actualDisplay2.mChildren[0] - assertThat(actualDisplay3.mDisplayId).isEqualTo(3) - assertThat(actualDisplay3.mWidth).isEqualTo(600f) - assertThat(actualDisplay3.mHeight).isEqualTo(200f) - assertThat(actualDisplay3.mPosition).isEqualTo(POSITION_BOTTOM) - assertThat(actualDisplay3.mOffset).isEqualTo(0f) - assertThat(actualDisplay3.mChildren).isEmpty() - } - - @Test - fun normalization_moveAndReparentDisplay() { - val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f, - /* height= */ 600f, /* position= */ null, /* offset= */ 0f) - topology.mRoot = display1 - - val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 200f, - /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) - display1.mChildren.add(display2) - - val primaryDisplayId = 3 - val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f, - /* height= */ 200f, POSITION_RIGHT, /* offset= */ 400f) - display1.mChildren.add(display3) - topology.mPrimaryDisplayId = primaryDisplayId - - val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f, - /* height= */ 600f, POSITION_RIGHT, /* offset= */ 0f) - display2.mChildren.add(display4) - - topology.normalize() - - assertThat(topology.mPrimaryDisplayId).isEqualTo(primaryDisplayId) - - val actualDisplay1 = topology.mRoot!! - assertThat(actualDisplay1.mDisplayId).isEqualTo(1) - assertThat(actualDisplay1.mWidth).isEqualTo(200f) - assertThat(actualDisplay1.mHeight).isEqualTo(600f) - assertThat(actualDisplay1.mChildren).hasSize(1) - - val actualDisplay2 = actualDisplay1.mChildren[0] - assertThat(actualDisplay2.mDisplayId).isEqualTo(2) - assertThat(actualDisplay2.mWidth).isEqualTo(200f) - assertThat(actualDisplay2.mHeight).isEqualTo(600f) - assertThat(actualDisplay2.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(actualDisplay2.mOffset).isEqualTo(0f) - assertThat(actualDisplay2.mChildren).hasSize(1) - - val actualDisplay3 = actualDisplay2.mChildren[0] - assertThat(actualDisplay3.mDisplayId).isEqualTo(3) - assertThat(actualDisplay3.mWidth).isEqualTo(600f) - assertThat(actualDisplay3.mHeight).isEqualTo(200f) - assertThat(actualDisplay3.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(actualDisplay3.mOffset).isEqualTo(400f) - assertThat(actualDisplay3.mChildren).hasSize(1) - - val actualDisplay4 = actualDisplay3.mChildren[0] - assertThat(actualDisplay4.mDisplayId).isEqualTo(4) - assertThat(actualDisplay4.mWidth).isEqualTo(200f) - assertThat(actualDisplay4.mHeight).isEqualTo(600f) - assertThat(actualDisplay4.mPosition).isEqualTo(POSITION_RIGHT) - assertThat(actualDisplay4.mOffset).isEqualTo(-400f) - assertThat(actualDisplay4.mChildren).isEmpty() - } -}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java index da79f301ee3c..2aafdfa8a4d3 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessClamperControllerTest.java @@ -98,6 +98,7 @@ public class BrightnessClamperControllerTest { @Mock private DeviceConfig.Properties mMockProperties; private BrightnessClamperController mClamperController; + private DisplayBrightnessState mDisplayBrightnessState; private TestInjector mTestInjector; @Before @@ -109,6 +110,7 @@ public class BrightnessClamperControllerTest { when(mMockDisplayDeviceData.getAmbientLightSensor()).thenReturn(mMockSensorData); mClamperController = createBrightnessClamperController(); + mDisplayBrightnessState = DisplayBrightnessState.builder().build(); } @Test @@ -192,7 +194,8 @@ public class BrightnessClamperControllerTest { public void testClamp_AppliesModifier() { float initialBrightness = 0.2f; boolean initialSlowChange = true; - mClamperController.clamp(mMockRequest, initialBrightness, initialSlowChange, STATE_ON); + mClamperController.clamp(mDisplayBrightnessState, mMockRequest, initialBrightness, + initialSlowChange, STATE_ON); verify(mMockModifier).apply(eq(mMockRequest), any()); verify(mMockDisplayListenerModifier).apply(eq(mMockRequest), any()); @@ -204,7 +207,8 @@ public class BrightnessClamperControllerTest { float initialBrightness = 0.2f; boolean initialSlowChange = true; when(mMockModifier.shouldListenToLightSensor()).thenReturn(true); - mClamperController.clamp(mMockRequest, initialBrightness, initialSlowChange, STATE_ON); + mClamperController.clamp(mDisplayBrightnessState, mMockRequest, initialBrightness, + initialSlowChange, STATE_ON); verify(mMockLightSensorController).restart(); } @@ -214,7 +218,8 @@ public class BrightnessClamperControllerTest { float initialBrightness = 0.2f; boolean initialSlowChange = true; clearInvocations(mMockLightSensorController); - mClamperController.clamp(mMockRequest, initialBrightness, initialSlowChange, STATE_OFF); + mClamperController.clamp(mDisplayBrightnessState, mMockRequest, initialBrightness, + initialSlowChange, STATE_OFF); verify(mMockLightSensorController).stop(); } @@ -232,8 +237,8 @@ public class BrightnessClamperControllerTest { mTestInjector.mCapturedChangeListener.onChanged(); mTestHandler.flush(); - DisplayBrightnessState state = mClamperController.clamp(mMockRequest, initialBrightness, - initialSlowChange, STATE_ON); + DisplayBrightnessState state = mClamperController.clamp(mDisplayBrightnessState, + mMockRequest, initialBrightness, initialSlowChange, STATE_ON); assertEquals(initialBrightness, state.getBrightness(), FLOAT_TOLERANCE); assertEquals(PowerManager.BRIGHTNESS_MAX, state.getMaxBrightness(), FLOAT_TOLERANCE); @@ -256,8 +261,8 @@ public class BrightnessClamperControllerTest { mTestInjector.mCapturedChangeListener.onChanged(); mTestHandler.flush(); - DisplayBrightnessState state = mClamperController.clamp(mMockRequest, initialBrightness, - initialSlowChange, STATE_ON); + DisplayBrightnessState state = mClamperController.clamp(mDisplayBrightnessState, + mMockRequest, initialBrightness, initialSlowChange, STATE_ON); assertEquals(clampedBrightness, state.getBrightness(), FLOAT_TOLERANCE); assertEquals(clampedBrightness, state.getMaxBrightness(), FLOAT_TOLERANCE); @@ -280,8 +285,8 @@ public class BrightnessClamperControllerTest { mTestInjector.mCapturedChangeListener.onChanged(); mTestHandler.flush(); - DisplayBrightnessState state = mClamperController.clamp(mMockRequest, initialBrightness, - initialSlowChange, STATE_ON); + DisplayBrightnessState state = mClamperController.clamp(mDisplayBrightnessState, + mMockRequest, initialBrightness, initialSlowChange, STATE_ON); assertEquals(initialBrightness, state.getBrightness(), FLOAT_TOLERANCE); assertEquals(clampedBrightness, state.getMaxBrightness(), FLOAT_TOLERANCE); @@ -304,11 +309,11 @@ public class BrightnessClamperControllerTest { mTestInjector.mCapturedChangeListener.onChanged(); mTestHandler.flush(); // first call of clamp method - mClamperController.clamp(mMockRequest, initialBrightness, + mClamperController.clamp(mDisplayBrightnessState, mMockRequest, initialBrightness, initialSlowChange, STATE_ON); // immediately second call of clamp method - DisplayBrightnessState state = mClamperController.clamp(mMockRequest, initialBrightness, - initialSlowChange, STATE_ON); + DisplayBrightnessState state = mClamperController.clamp(mDisplayBrightnessState, + mMockRequest, initialBrightness, initialSlowChange, STATE_ON); assertEquals(clampedBrightness, state.getBrightness(), FLOAT_TOLERANCE); assertEquals(clampedBrightness, state.getMaxBrightness(), FLOAT_TOLERANCE); @@ -319,6 +324,22 @@ public class BrightnessClamperControllerTest { } @Test + public void testClamp_activeClamperApplied_confirmBrightnessOverrideStateReturned() { + float initialBrightness = 0.8f; + boolean initialSlowChange = false; + mTestInjector.mCapturedChangeListener.onChanged(); + mTestHandler.flush(); + + mDisplayBrightnessState = DisplayBrightnessState.builder().setBrightnessReason( + BrightnessReason.REASON_OVERRIDE).build(); + + DisplayBrightnessState state = mClamperController.clamp(mDisplayBrightnessState, + mMockRequest, initialBrightness, initialSlowChange, STATE_ON); + + assertEquals(BrightnessReason.REASON_OVERRIDE, state.getBrightnessReason().getReason()); + } + + @Test public void testAmbientLuxChanges() { mTestInjector.mCapturedLightSensorListener.onAmbientLuxChange(50); diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt index 3c77ec925078..3aef6aa2ee3f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt @@ -97,11 +97,14 @@ class BrightnessObserverTest { private fun setUpLowBrightnessZone() { whenever(mockInjector.getBrightnessInfo(Display.DEFAULT_DISPLAY)).thenReturn( - BrightnessInfo(/* brightness = */ 0.05f, /* adjustedBrightness = */ 0.05f, - /* brightnessMinimum = */ 0.0f, /* brightnessMaximum = */ 1.0f, - BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, - /* highBrightnessTransitionPoint = */ 1.0f, - BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE)) + BrightnessInfo(/* brightness = */ 0.05f, /* adjustedBrightness = */ 0.05f, + /* brightnessMinimum = */ 0.0f, /* brightnessMaximum = */ 1.0f, + BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, + /* highBrightnessTransitionPoint = */ 1.0f, + BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, + false /* isBrightnessOverrideByWindow */ + ) + ) whenever(mockDeviceConfig.highDisplayBrightnessThresholds).thenReturn(floatArrayOf()) whenever(mockDeviceConfig.highAmbientBrightnessThresholds).thenReturn(floatArrayOf()) whenever(mockDeviceConfig.lowDisplayBrightnessThresholds).thenReturn(floatArrayOf(0.1f)) diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index f3fc6d747d78..4e0bab8bf4bd 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -3786,8 +3786,9 @@ public class DisplayModeDirectorTest { when(mInjector.getBrightnessInfo(DISPLAY_ID)).thenReturn( new BrightnessInfo(floatBri, floatAdjBri, 0.0f, 1.0f, - BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, TRANSITION_POINT, - BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE)); + BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, TRANSITION_POINT, + BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, + false /* isBrightnessOverrideByWindow */)); listener.onDisplayChanged(DISPLAY_ID); } diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java index 685e8d6a3bc5..e611867493eb 100644 --- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubServiceTest.java @@ -65,7 +65,7 @@ public class ContextHubServiceTest { new Pair<>(Arrays.asList(mMockContextHubInfo), Arrays.asList("")); when(mMockContextHubInfo.getId()).thenReturn(CONTEXT_HUB_ID); when(mMockContextHubInfo.toString()).thenReturn(CONTEXT_HUB_STRING); - when(mMockContextHubWrapper.getHubs()).thenReturn(hubInfo); + when(mMockContextHubWrapper.getContextHubs()).thenReturn(hubInfo); when(mMockContextHubWrapper.supportsLocationSettingNotifications()).thenReturn(true); when(mMockContextHubWrapper.supportsWifiSettingNotifications()).thenReturn(true); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java index 0b89c11a11f4..38ff3a22022d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java @@ -2291,7 +2291,9 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_CLASSIFICATION, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) public void testMoveAggregateGroups_updateChannel_multipleChannels_regroupOnClassifEnabled() { final String pkg = "package"; final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg, @@ -2366,7 +2368,9 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_CLASSIFICATION, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) public void testMoveSections_notificationBundled() { final List<NotificationRecord> notificationList = new ArrayList<>(); final String pkg = "package"; @@ -2436,7 +2440,9 @@ public class GroupHelperTest extends UiServiceTestCase { } @Test - @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_CLASSIFICATION, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) public void testCacheAndCancelAppSummary_notificationBundled() { // check that the original app summary is canceled & cached on classification regrouping final List<NotificationRecord> notificationList = new ArrayList<>(); @@ -2495,6 +2501,7 @@ public class GroupHelperTest extends UiServiceTestCase { @Test @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS}) public void testSingletonGroupsRegrouped_notificationBundledBeforeDelayTimeout() { @@ -2569,6 +2576,7 @@ public class GroupHelperTest extends UiServiceTestCase { @Test @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS}) public void testSingletonGroupsRegrouped_notificationBundledAfterDelayTimeout() { diff --git a/services/tests/vibrator/AndroidManifest.xml b/services/tests/vibrator/AndroidManifest.xml index c0f514fb9673..850884f84b01 100644 --- a/services/tests/vibrator/AndroidManifest.xml +++ b/services/tests/vibrator/AndroidManifest.xml @@ -32,6 +32,9 @@ <uses-permission android:name="android.permission.VIBRATE_ALWAYS_ON" /> <!-- Required to play system-only haptic feedback constants --> <uses-permission android:name="android.permission.VIBRATE_SYSTEM_CONSTANTS" /> + <!-- Required to play vendor effects and start vendor sessions --> + <uses-permission android:name="android.permission.VIBRATE_VENDOR_EFFECTS" /> + <uses-permission android:name="android.permission.START_VIBRATION_SESSIONS" /> <application android:debuggable="true"> <uses-library android:name="android.test.mock" android:required="true" /> diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index dfdd0cde6aba..88ba9e3af6df 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -35,6 +35,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; @@ -83,6 +84,8 @@ import android.os.Vibrator; import android.os.VibratorInfo; import android.os.test.FakeVibrator; import android.os.test.TestLooper; +import android.os.vibrator.IVibrationSession; +import android.os.vibrator.IVibrationSessionCallback; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; import android.os.vibrator.StepSegment; @@ -195,6 +198,7 @@ public class VibratorManagerServiceTest { new SparseArray<>(); private final List<HalVibration> mPendingVibrations = new ArrayList<>(); + private final List<VendorVibrationSession> mPendingSessions = new ArrayList<>(); private VibratorManagerService mService; private Context mContextSpy; @@ -264,6 +268,11 @@ public class VibratorManagerServiceTest { grantPermission(android.Manifest.permission.VIBRATE); // Cancel any pending vibration from tests, including external vibrations. cancelVibrate(mService); + // End pending sessions. + for (VendorVibrationSession session : mPendingSessions) { + session.cancelSession(); + } + mTestLooper.dispatchAll(); // Wait until pending vibrations end asynchronously. for (HalVibration vibration : mPendingVibrations) { vibration.waitForEnd(); @@ -1229,6 +1238,36 @@ public class VibratorManagerServiceTest { .anyMatch(PrebakedSegment.class::isInstance)); } + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @Test + public void vibrate_withOngoingHigherImportanceSession_ignoresEffect() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + verify(callback).onStarted(any(IVibrationSession.class)); + + HalVibration vibration = vibrateAndWaitUntilFinished(service, + VibrationEffect.get(VibrationEffect.EFFECT_CLICK), + HAPTIC_FEEDBACK_ATTRS); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + assertThat(vibration.getStatus()).isEqualTo(Status.IGNORED_FOR_HIGHER_IMPORTANCE); + verify(callback, never()).onFinishing(); + verify(callback, never()).onFinished(anyInt()); + // The second vibration shouldn't have played any prebaked segment. + assertFalse(fakeVibrator.getAllEffectSegments().stream() + .anyMatch(PrebakedSegment.class::isInstance)); + } + @Test public void vibrate_withOngoingLowerImportanceVibration_cancelsOngoingEffect() throws Exception { @@ -1289,6 +1328,36 @@ public class VibratorManagerServiceTest { .filter(PrebakedSegment.class::isInstance).count()); } + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @Test + public void vibrate_withOngoingLowerImportanceSession_cancelsOngoingSession() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + verify(callback).onStarted(any(IVibrationSession.class)); + + HalVibration vibration = vibrateAndWaitUntilFinished(service, + VibrationEffect.get(VibrationEffect.EFFECT_CLICK), + HAPTIC_FEEDBACK_ATTRS); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED); + assertThat(vibration.getStatus()).isEqualTo(Status.FINISHED); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED)); + // One segment played is the prebaked CLICK from the new vibration. + assertEquals(1, mVibratorProviders.get(1).getAllEffectSegments().stream() + .filter(PrebakedSegment.class::isInstance).count()); + } + @Test public void vibrate_withOngoingSameImportancePipelinedVibration_continuesOngoingEffect() throws Exception { @@ -1416,16 +1485,16 @@ public class VibratorManagerServiceTest { // The native callback will be dispatched manually in this test. mTestLooper.stopAutoDispatchAndIgnoreExceptions(); - ArgumentCaptor<VibratorManagerService.OnSyncedVibrationCompleteListener> listenerCaptor = + ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor = ArgumentCaptor.forClass( - VibratorManagerService.OnSyncedVibrationCompleteListener.class); + VibratorManagerService.VibratorManagerNativeCallbacks.class); verify(mNativeWrapperMock).init(listenerCaptor.capture()); CountDownLatch triggerCountDown = new CountDownLatch(1); // Mock trigger callback on registered listener right after the synced vibration starts. when(mNativeWrapperMock.prepareSynced(eq(new int[]{1, 2}))).thenReturn(true); when(mNativeWrapperMock.triggerSynced(anyLong())).then(answer -> { - listenerCaptor.getValue().onComplete(answer.getArgument(0)); + listenerCaptor.getValue().onSyncedVibrationComplete(answer.getArgument(0)); triggerCountDown.countDown(); return true; }); @@ -2318,6 +2387,34 @@ public class VibratorManagerServiceTest { assertEquals(Arrays.asList(false), mVibratorProviders.get(1).getExternalControlStates()); } + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @Test + public void onExternalVibration_withOngoingHigherImportanceSession_ignoreNewVibration() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + verify(callback).onStarted(any(IVibrationSession.class)); + + ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class)); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + // External vibration is ignored. + assertEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); + + // Session still running. + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + verify(callback, never()).onFinishing(); + verify(callback, never()).onFinished(anyInt()); + } + @Test public void onExternalVibration_withNewSameImportanceButRepeating_cancelsOngoingVibration() throws Exception { @@ -2373,6 +2470,36 @@ public class VibratorManagerServiceTest { assertEquals(Arrays.asList(false), mVibratorProviders.get(1).getExternalControlStates()); } + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + @Test + public void onExternalVibration_withOngoingLowerImportanceSession_cancelsOngoingSession() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + verify(callback).onStarted(any(IVibrationSession.class)); + + ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + AUDIO_ALARM_ATTRS, mock(IExternalVibrationController.class)); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); + mTestLooper.dispatchAll(); + + // Session is cancelled. + assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED)); + assertEquals(Arrays.asList(false, true), + mVibratorProviders.get(1).getExternalControlStates()); + } + @Test public void onExternalVibration_withRingtone_usesRingerModeSettings() { mockVibrators(1); @@ -2638,6 +2765,376 @@ public class VibratorManagerServiceTest { } @Test + @DisableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withoutFeatureFlag_throwsException() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + int vibratorId = 1; + mockVibrators(vibratorId); + VibratorManagerService service = createSystemReadyService(); + + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + assertThrows("Expected starting session without feature flag to fail!", + UnsupportedOperationException.class, + () -> startSession(service, RINGTONE_ATTRS, callback, vibratorId)); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class)); + verify(callback, never()).onStarted(any(IVibrationSession.class)); + verify(callback, never()).onFinishing(); + verify(callback, never()).onFinished(anyInt()); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withoutCapability_doesNotStart() throws Exception { + int vibratorId = 1; + mockVibrators(vibratorId); + VibratorManagerService service = createSystemReadyService(); + + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, + callback, vibratorId); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED); + verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class)); + verify(callback, never()).onFinishing(); + verify(callback) + .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withoutCallback_doesNotStart() { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + int vibratorId = 1; + mockVibrators(vibratorId); + VibratorManagerService service = createSystemReadyService(); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, + /* callback= */ null, vibratorId); + mTestLooper.dispatchAll(); + + assertThat(session).isNull(); + verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withoutVibratorIds_doesNotStart() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + VibratorManagerService service = createSystemReadyService(); + + int[] nullIds = null; + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, nullIds); + assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED); + + int[] emptyIds = {}; + session = startSession(service, RINGTONE_ATTRS, callback, emptyIds); + assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED); + + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock, never()).startSession(anyLong(), any(int[].class)); + verify(callback, never()).onFinishing(); + verify(callback, times(2)) + .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_badVibratorId_failsToStart() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + when(mNativeWrapperMock.startSession(anyLong(), any(int[].class))).thenReturn(false); + doReturn(false).when(mNativeWrapperMock).startSession(anyLong(), eq(new int[] {1, 3})); + doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), eq(new int[] {1, 2})); + VibratorManagerService service = createSystemReadyService(); + + IBinder token = mock(IBinder.class); + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + doReturn(token).when(callback).asBinder(); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 3); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.IGNORED_UNSUPPORTED); + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 3})); + verify(callback, never()).onStarted(any(IVibrationSession.class)); + verify(callback, never()).onFinishing(); + verify(callback) + .onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_UNSUPPORTED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_thenFinish_returnsSuccessAfterCallback() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + VibratorManagerService service = createSystemReadyService(); + int sessionFinishDelayMs = 200; + IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2})); + ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class); + verify(callback).onStarted(captor.capture()); + + captor.getValue().finishSession(); + + // Session not ended until HAL callback. + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + + // Dispatch HAL callbacks. + mTestLooper.moveTimeForward(sessionFinishDelayMs); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.FINISHED); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_thenSendCancelSignal_cancelsSession() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + VibratorManagerService service = createSystemReadyService(); + int sessionFinishDelayMs = 200; + IVibrationSessionCallback callback = mockSessionCallbacks(sessionFinishDelayMs); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2})); + ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class); + verify(callback).onStarted(captor.capture()); + + session.getCancellationSignal().cancel(); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_USER); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_thenCancel_returnsCancelStatus() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + VibratorManagerService service = createSystemReadyService(); + // Delay not applied when session is aborted. + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2})); + ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class); + verify(callback).onStarted(captor.capture()); + + captor.getValue().cancelSession(); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_USER); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_finishThenCancel_returnsRightAwayWithFinishedStatus() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + VibratorManagerService service = createSystemReadyService(); + // Delay not applied when session is aborted. + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2})); + ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class); + verify(callback).onStarted(captor.capture()); + + captor.getValue().finishSession(); + mTestLooper.dispatchAll(); + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + + captor.getValue().cancelSession(); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.FINISHED); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_thenHalCancels_returnsCancelStatus() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1, 2); + VibratorManagerService service = createSystemReadyService(); + ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor = + ArgumentCaptor.forClass( + VibratorManagerService.VibratorManagerNativeCallbacks.class); + verify(mNativeWrapperMock).init(listenerCaptor.capture()); + doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), any(int[].class)); + + IBinder token = mock(IBinder.class); + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + doReturn(token).when(callback).asBinder(); + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1, 2); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] {1, 2})); + verify(callback).onStarted(any(IVibrationSession.class)); + + // Mock HAL ending session unexpectedly. + listenerCaptor.getValue().onVibrationSessionComplete(session.getSessionId()); + mTestLooper.dispatchAll(); + + assertThat(session.getStatus()).isEqualTo(Status.CANCELLED_BY_UNKNOWN_REASON); + verify(callback).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_CANCELED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withPowerMode_usesPowerModeState() throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + mRegisteredPowerModeListener.onLowPowerModeChanged(LOW_POWER_STATE); + VendorVibrationSession session1 = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1); + VendorVibrationSession session2 = startSession(service, RINGTONE_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + + ArgumentCaptor<IVibrationSession> captor = ArgumentCaptor.forClass(IVibrationSession.class); + verify(callback).onStarted(captor.capture()); + captor.getValue().cancelSession(); + mTestLooper.dispatchAll(); + + mRegisteredPowerModeListener.onLowPowerModeChanged(NORMAL_POWER_STATE); + VendorVibrationSession session3 = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock, never()) + .startSession(eq(session1.getSessionId()), any(int[].class)); + verify(mNativeWrapperMock).startSession(eq(session2.getSessionId()), eq(new int[] {1})); + verify(mNativeWrapperMock).startSession(eq(session3.getSessionId()), eq(new int[] {1})); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withOngoingHigherImportanceVibration_ignoresSession() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VibrationEffect effect = VibrationEffect.createWaveform( + new long[]{10, 10_000}, new int[]{128, 255}, -1); + vibrate(service, effect, ALARM_ATTRS); + + // VibrationThread will start this vibration async. + // Wait until second step started to ensure the noteVibratorOn was triggered. + assertTrue(waitUntil(s -> fakeVibrator.getAmplitudes().size() == 2, + service, TEST_TIMEOUT_MILLIS)); + + VendorVibrationSession session = startSession(service, HAPTIC_FEEDBACK_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + + verify(mNativeWrapperMock, never()) + .startSession(eq(session.getSessionId()), any(int[].class)); + assertThat(session.getStatus()).isEqualTo(Status.IGNORED_FOR_HIGHER_IMPORTANCE); + verify(callback, never()).onFinishing(); + verify(callback).onFinished(eq(android.os.vibrator.VendorVibrationSession.STATUS_IGNORED)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withOngoingLowerImportanceVibration_cancelsOngoing() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(1); + fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL); + fakeVibrator.setSupportedEffects(VibrationEffect.EFFECT_CLICK); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + VibrationEffect effect = VibrationEffect.createWaveform( + new long[]{10, 10_000}, new int[]{128, 255}, -1); + HalVibration vibration = vibrate(service, effect, HAPTIC_FEEDBACK_ATTRS); + + // VibrationThread will start this vibration async. + // Wait until second step started to ensure the noteVibratorOn was triggered. + assertTrue(waitUntil(s -> fakeVibrator.getAmplitudes().size() == 2, service, + TEST_TIMEOUT_MILLIS)); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1); + vibration.waitForEnd(); + assertTrue(waitUntil(s -> session.isStarted(), service, TEST_TIMEOUT_MILLIS)); + mTestLooper.dispatchAll(); + + assertThat(vibration.getStatus()).isEqualTo(Status.CANCELLED_SUPERSEDED); + assertThat(session.getStatus()).isEqualTo(Status.RUNNING); + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] { 1 })); + verify(callback).onStarted(any(IVibrationSession.class)); + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS) + public void startVibrationSession_withOngoingLowerImportanceExternalVibration_cancelsOngoing() + throws Exception { + mockCapabilities(IVibratorManager.CAP_START_SESSIONS); + mockVibrators(1); + mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); + mVibratorProviders.get(1).setSupportedEffects(VibrationEffect.EFFECT_CLICK); + setRingerMode(AudioManager.RINGER_MODE_NORMAL); + VibratorManagerService service = createSystemReadyService(); + IVibrationSessionCallback callback = + mockSessionCallbacks(/* delayToEndSessionMillis= */ TEST_TIMEOUT_MILLIS); + + IBinder firstToken = mock(IBinder.class); + IExternalVibrationController controller = mock(IExternalVibrationController.class); + ExternalVibration externalVibration = new ExternalVibration(UID, PACKAGE_NAME, + AUDIO_ALARM_ATTRS, + controller, firstToken); + ExternalVibrationScale scale = + mExternalVibratorService.onExternalVibrationStart(externalVibration); + + VendorVibrationSession session = startSession(service, RINGTONE_ATTRS, callback, 1); + mTestLooper.dispatchAll(); + + assertNotEquals(ExternalVibrationScale.ScaleLevel.SCALE_MUTE, scale.scaleLevel); + // The external vibration should have been cancelled + verify(controller).mute(); + assertEquals(Arrays.asList(false, true, false), + mVibratorProviders.get(1).getExternalControlStates()); + verify(mNativeWrapperMock).startSession(eq(session.getSessionId()), eq(new int[] { 1 })); + verify(callback).onStarted(any(IVibrationSession.class)); + } + + @Test public void frameworkStats_externalVibration_reportsAllMetrics() throws Exception { mockVibrators(1); mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL); @@ -3050,6 +3547,30 @@ public class VibratorManagerServiceTest { when(mNativeWrapperMock.getVibratorIds()).thenReturn(vibratorIds); } + private IVibrationSessionCallback mockSessionCallbacks(long delayToEndSessionMillis) { + Handler handler = new Handler(mTestLooper.getLooper()); + ArgumentCaptor<VibratorManagerService.VibratorManagerNativeCallbacks> listenerCaptor = + ArgumentCaptor.forClass( + VibratorManagerService.VibratorManagerNativeCallbacks.class); + verify(mNativeWrapperMock).init(listenerCaptor.capture()); + doReturn(true).when(mNativeWrapperMock).startSession(anyLong(), any(int[].class)); + doAnswer(args -> { + handler.postDelayed( + () -> listenerCaptor.getValue().onVibrationSessionComplete(args.getArgument(0)), + delayToEndSessionMillis); + return null; + }).when(mNativeWrapperMock).endSession(anyLong(), eq(false)); + doAnswer(args -> { + listenerCaptor.getValue().onVibrationSessionComplete(args.getArgument(0)); + return null; + }).when(mNativeWrapperMock).endSession(anyLong(), eq(true)); + + IBinder token = mock(IBinder.class); + IVibrationSessionCallback callback = mock(IVibrationSessionCallback.class); + doReturn(token).when(callback).asBinder(); + return callback; + } + private void cancelVibrate(VibratorManagerService service) { service.cancelVibrate(VibrationAttributes.USAGE_FILTER_MATCH_ALL, service); } @@ -3157,6 +3678,16 @@ public class VibratorManagerServiceTest { return vib; } + private VendorVibrationSession startSession(VibratorManagerService service, + VibrationAttributes attrs, IVibrationSessionCallback callback, int... vibratorIds) { + VendorVibrationSession session = service.startVendorVibrationSessionInternal(UID, + Context.DEVICE_ID_DEFAULT, PACKAGE_NAME, vibratorIds, attrs, "reason", callback); + if (session != null) { + mPendingSessions.add(session); + } + return session; + } + private boolean waitUntil(Predicate<VibratorManagerService> predicate, VibratorManagerService service, long timeout) throws InterruptedException { long timeoutTimestamp = SystemClock.uptimeMillis() + timeout; diff --git a/telecomm/java/android/telecom/Log.java b/telecomm/java/android/telecom/Log.java index a34094ce6452..98949d0c45cf 100644 --- a/telecomm/java/android/telecom/Log.java +++ b/telecomm/java/android/telecom/Log.java @@ -68,7 +68,7 @@ public class Log { // Used to synchronize singleton logging lazy initialization private static final Object sSingletonSync = new Object(); private static EventManager sEventManager; - private static SessionManager sSessionManager; + private static volatile SessionManager sSessionManager; private static Object sLock = null; /** @@ -379,6 +379,23 @@ public class Log { return sSessionManager; } + @VisibleForTesting + public static SessionManager setSessionManager(Context context, + java.lang.Runnable cleanSessionRunnable) { + // Checking for null again outside of synchronization because we only need to synchronize + // during the lazy loading of the session logger. We don't need to synchronize elsewhere. + if (sSessionManager == null) { + synchronized (sSingletonSync) { + if (sSessionManager == null) { + sSessionManager = new SessionManager(cleanSessionRunnable); + sSessionManager.setContext(context); + return sSessionManager; + } + } + } + return sSessionManager; + } + public static void setTag(String tag) { TAG = tag; DEBUG = isLoggable(android.util.Log.DEBUG); diff --git a/telecomm/java/android/telecom/Logging/SessionManager.java b/telecomm/java/android/telecom/Logging/SessionManager.java index 00e344c67cc5..ac1e69e92ec0 100644 --- a/telecomm/java/android/telecom/Logging/SessionManager.java +++ b/telecomm/java/android/telecom/Logging/SessionManager.java @@ -62,9 +62,7 @@ public class SessionManager { @VisibleForTesting public final ConcurrentHashMap<Integer, Session> mSessionMapper = new ConcurrentHashMap<>(64); - @VisibleForTesting - public java.lang.Runnable mCleanStaleSessions = () -> - cleanupStaleSessions(getSessionCleanupTimeoutMs()); + private final java.lang.Runnable mCleanStaleSessions; private final Handler mSessionCleanupHandler = new Handler(Looper.getMainLooper()); // Overridden in LogTest to skip query to ContentProvider @@ -110,29 +108,39 @@ public class SessionManager { } public SessionManager() { + mCleanStaleSessions = () -> cleanupStaleSessions(getSessionCleanupTimeoutMs()); + } + + @VisibleForTesting + public SessionManager(java.lang.Runnable cleanStaleSessionsRunnable) { + mCleanStaleSessions = cleanStaleSessionsRunnable; } private long getSessionCleanupTimeoutMs() { return mSessionCleanupTimeoutMs.get(); } - private synchronized void resetStaleSessionTimer() { + private void resetStaleSessionTimer() { if (!Flags.endSessionImprovements()) { - mSessionCleanupHandler.removeCallbacksAndMessages(null); - // Will be null in Log Testing - if (mCleanStaleSessions != null) { - mSessionCleanupHandler.postDelayed(mCleanStaleSessions, - getSessionCleanupTimeoutMs()); - } - } else { - if (mCleanStaleSessions != null - && !mSessionCleanupHandler.hasCallbacks(mCleanStaleSessions)) { + resetStaleSessionTimerOld(); + return; + } + // Will be null in Log Testing + if (mCleanStaleSessions == null) return; + synchronized (mSessionCleanupHandler) { + if (!mSessionCleanupHandler.hasCallbacks(mCleanStaleSessions)) { mSessionCleanupHandler.postDelayed(mCleanStaleSessions, getSessionCleanupTimeoutMs()); } } } + private synchronized void resetStaleSessionTimerOld() { + if (mCleanStaleSessions == null) return; + mSessionCleanupHandler.removeCallbacksAndMessages(null); + mSessionCleanupHandler.postDelayed(mCleanStaleSessions, getSessionCleanupTimeoutMs()); + } + /** * Determines whether or not to start a new session or continue an existing session based on * the {@link Session.Info} info passed into startSession. If info is null, a new Session is diff --git a/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt b/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt index 862886ce69d2..e281a3fb1287 100644 --- a/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt +++ b/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt @@ -61,13 +61,13 @@ class InputGestureManagerTests { assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, result) assertEquals( listOf(customGesture), - inputGestureManager.getCustomInputGestures(USER_ID) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) ) inputGestureManager.removeCustomInputGesture(USER_ID, customGesture) assertEquals( listOf<InputGestureData>(), - inputGestureManager.getCustomInputGestures(USER_ID) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) ) } @@ -86,7 +86,7 @@ class InputGestureManagerTests { assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST, result) assertEquals( listOf<InputGestureData>(), - inputGestureManager.getCustomInputGestures(USER_ID) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) ) } @@ -115,7 +115,7 @@ class InputGestureManagerTests { assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS, result) assertEquals( listOf(customGesture), - inputGestureManager.getCustomInputGestures(USER_ID) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) ) } @@ -144,13 +144,67 @@ class InputGestureManagerTests { assertEquals( listOf(customGesture, customGesture2), - inputGestureManager.getCustomInputGestures(USER_ID) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) ) - inputGestureManager.removeAllCustomInputGestures(USER_ID) + inputGestureManager.removeAllCustomInputGestures(USER_ID, /* filter = */null) assertEquals( listOf<InputGestureData>(), - inputGestureManager.getCustomInputGestures(USER_ID) + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + ) + } + + @Test + fun filteringBasedOnTouchpadOrKeyGestures() { + val customKeyGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + inputGestureManager.addCustomInputGesture(USER_ID, customKeyGesture) + val customTouchpadGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createTouchpadTrigger( + InputGestureData.TOUCHPAD_GESTURE_TYPE_THREE_FINGER_TAP + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() + inputGestureManager.addCustomInputGesture(USER_ID, customTouchpadGesture) + + assertEquals( + listOf(customTouchpadGesture, customKeyGesture), + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + ) + assertEquals( + listOf(customKeyGesture), + inputGestureManager.getCustomInputGestures(USER_ID, InputGestureData.Filter.KEY) + ) + assertEquals( + listOf(customTouchpadGesture), + inputGestureManager.getCustomInputGestures( + USER_ID, + InputGestureData.Filter.TOUCHPAD + ) + ) + + inputGestureManager.removeAllCustomInputGestures(USER_ID, InputGestureData.Filter.KEY) + assertEquals( + listOf(customTouchpadGesture), + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) + ) + + inputGestureManager.removeAllCustomInputGestures( + USER_ID, + InputGestureData.Filter.TOUCHPAD + ) + assertEquals( + listOf<InputGestureData>(), + inputGestureManager.getCustomInputGestures(USER_ID, /* filter = */null) ) } }
\ No newline at end of file diff --git a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java index 07b733830bd3..0da4521fca71 100644 --- a/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java +++ b/tests/permission/src/com/android/framework/permission/tests/VibratorManagerServicePermissionTest.java @@ -143,6 +143,38 @@ public class VibratorManagerServicePermissionTest { } @Test + public void testStartVendorVibrationSessionWithoutVibratePermissionFails() throws Exception { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.VIBRATE_VENDOR_EFFECTS, + Manifest.permission.START_VIBRATION_SESSIONS); + expectSecurityException("VIBRATE"); + mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME, + new int[] { 1 }, ATTRS, "testVibrate", null); + } + + @Test + public void testStartVendorVibrationSessionWithoutVibrateVendorEffectsPermissionFails() + throws Exception { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.VIBRATE, + Manifest.permission.START_VIBRATION_SESSIONS); + expectSecurityException("VIBRATE"); + mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME, + new int[] { 1 }, ATTRS, "testVibrate", null); + } + + @Test + public void testStartVendorVibrationSessionWithoutStartSessionPermissionFails() + throws Exception { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( + Manifest.permission.VIBRATE, + Manifest.permission.VIBRATE_VENDOR_EFFECTS); + expectSecurityException("VIBRATE"); + mVibratorService.startVendorVibrationSession(Process.myUid(), DEVICE_ID, PACKAGE_NAME, + new int[] { 1 }, ATTRS, "testVibrate", null); + } + + @Test public void testCancelVibrateFails() throws RemoteException { expectSecurityException("VIBRATE"); mVibratorService.cancelVibrate(/* usageFilter= */ -1, new Binder()); |