diff options
1055 files changed, 35407 insertions, 15327 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 6ecd38f054aa..3391698ee15a 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -335,6 +335,11 @@ java_aconfig_library { aconfig_declarations: "android.os.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], mode: "exported", + min_sdk_version: "30", + apex_available: [ + "//apex_available:platform", + "com.android.mediaprovider", + ], } cc_aconfig_library { @@ -716,6 +721,7 @@ aconfig_declarations { name: "android.credentials.flags-aconfig", package: "android.credentials.flags", srcs: ["core/java/android/credentials/flags.aconfig"], + exportable: true, } java_aconfig_library { @@ -724,6 +730,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "android.credentials.flags-aconfig-java-export", + aconfig_declarations: "android.credentials.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], + mode: "exported", +} + // Content Protection aconfig_declarations { name: "android.view.contentprotection.flags-aconfig", diff --git a/Android.bp b/Android.bp index 057b1d62ea5a..59e903ef37d3 100644 --- a/Android.bp +++ b/Android.bp @@ -389,7 +389,6 @@ java_defaults { // TODO(b/120066492): remove gps_debug and protolog.conf.json when the build // system propagates "required" properly. "gps_debug.conf", - "protolog.conf.json.gz", "core.protolog.pb", "framework-res", // any install dependencies should go into framework-minus-apex-install-dependencies diff --git a/TEST_MAPPING b/TEST_MAPPING index c904eb46d88e..49384cde5803 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -232,30 +232,5 @@ } ] } - ], - "auto-features-postsubmit": [ - // Test tag for automotive feature targets. These are only running in postsubmit. - // This tag is used in targeted test features testing to limit resource use. - // TODO(b/256932212): this tag to be removed once the above is no longer in use. - { - "name": "FrameworksMockingServicesTests", - "options": [ - { - "include-filter": "com.android.server.pm.UserVisibilityMediatorSUSDTest" - }, - { - "include-filter": "com.android.server.pm.UserVisibilityMediatorMUMDTest" - }, - { - "include-filter": "com.android.server.pm.UserVisibilityMediatorMUPANDTest" - }, - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - }, - { - "exclude-annotation": "org.junit.Ignore" - } - ] - } ] } diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig index 788e82407926..2c1a8532568c 100644 --- a/apex/jobscheduler/framework/aconfig/job.aconfig +++ b/apex/jobscheduler/framework/aconfig/job.aconfig @@ -9,6 +9,7 @@ flag { flag { name: "job_debug_info_apis" + is_exported: true namespace: "backstage_power" description: "Add APIs to let apps attach debug information to jobs" bug: "293491637" @@ -16,6 +17,7 @@ flag { flag { name: "backup_jobs_exemption" + is_exported: true namespace: "backstage_power" description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content." bug: "318731461" diff --git a/core/api/current.txt b/core/api/current.txt index 4d3ca1335416..982ab640af7c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10731,6 +10731,7 @@ package android.content { field public static final String DROPBOX_SERVICE = "dropbox"; field public static final String EUICC_SERVICE = "euicc"; field public static final String FILE_INTEGRITY_SERVICE = "file_integrity"; + field public static final String FINGERPRINT_SERVICE = "fingerprint"; field public static final String GAME_SERVICE = "game"; field public static final String GRAMMATICAL_INFLECTION_SERVICE = "grammatical_inflection"; field public static final String HARDWARE_PROPERTIES_SERVICE = "hardware_properties"; @@ -10764,6 +10765,7 @@ package android.content { field public static final String OVERLAY_SERVICE = "overlay"; field public static final String PEOPLE_SERVICE = "people"; field public static final String PERFORMANCE_HINT_SERVICE = "performance_hint"; + field @FlaggedApi("android.security.frp_enforcement") public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; field public static final String POWER_SERVICE = "power"; field public static final String PRINT_SERVICE = "print"; field @FlaggedApi("android.os.telemetry_apis_framework_initialization") public static final String PROFILING_SERVICE = "profiling"; @@ -12505,7 +12507,7 @@ package android.content.pm { method public boolean hasShortcutHostPermission(); method @RequiresPermission(conditional=true, anyOf={"android.permission.ACCESS_HIDDEN_PROFILES_FULL", android.Manifest.permission.ACCESS_HIDDEN_PROFILES}) public boolean isActivityEnabled(android.content.ComponentName, android.os.UserHandle); method @RequiresPermission(conditional=true, anyOf={"android.permission.ACCESS_HIDDEN_PROFILES_FULL", android.Manifest.permission.ACCESS_HIDDEN_PROFILES}) public boolean isPackageEnabled(String, android.os.UserHandle); - method public void pinShortcuts(@NonNull String, @NonNull java.util.List<java.lang.String>, @NonNull android.os.UserHandle); + method @RequiresPermission(conditional=true, value="android.permission.ACCESS_SHORTCUTS") public void pinShortcuts(@NonNull String, @NonNull java.util.List<java.lang.String>, @NonNull android.os.UserHandle); method @RequiresPermission(conditional=true, anyOf={"android.permission.ACCESS_HIDDEN_PROFILES_FULL", android.Manifest.permission.ACCESS_HIDDEN_PROFILES}) public void registerCallback(android.content.pm.LauncherApps.Callback); method @RequiresPermission(conditional=true, anyOf={"android.permission.ACCESS_HIDDEN_PROFILES_FULL", android.Manifest.permission.ACCESS_HIDDEN_PROFILES}) public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler); method public void registerPackageInstallerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageInstaller.SessionCallback); @@ -20235,10 +20237,10 @@ package android.hardware.camera2.params { method public android.hardware.camera2.CaptureRequest getSessionParameters(); method public int getSessionType(); method public android.hardware.camera2.CameraCaptureSession.StateCallback getStateCallback(); - method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback); method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named); method public void setInputConfiguration(@NonNull android.hardware.camera2.params.InputConfiguration); method public void setSessionParameters(android.hardware.camera2.CaptureRequest); + method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public void setStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.params.SessionConfiguration> CREATOR; field public static final int SESSION_HIGH_SPEED = 1; // 0x1 @@ -20386,6 +20388,54 @@ package android.hardware.display { } +package android.hardware.fingerprint { + + @Deprecated public class FingerprintManager { + method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.USE_BIOMETRIC, android.Manifest.permission.USE_FINGERPRINT}) public void authenticate(@Nullable android.hardware.fingerprint.FingerprintManager.CryptoObject, @Nullable android.os.CancellationSignal, int, @NonNull android.hardware.fingerprint.FingerprintManager.AuthenticationCallback, @Nullable android.os.Handler); + method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean hasEnrolledFingerprints(); + method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean isHardwareDetected(); + field public static final int FINGERPRINT_ACQUIRED_GOOD = 0; // 0x0 + field public static final int FINGERPRINT_ACQUIRED_IMAGER_DIRTY = 3; // 0x3 + field public static final int FINGERPRINT_ACQUIRED_INSUFFICIENT = 2; // 0x2 + field public static final int FINGERPRINT_ACQUIRED_PARTIAL = 1; // 0x1 + field public static final int FINGERPRINT_ACQUIRED_TOO_FAST = 5; // 0x5 + field public static final int FINGERPRINT_ACQUIRED_TOO_SLOW = 4; // 0x4 + field public static final int FINGERPRINT_ERROR_CANCELED = 5; // 0x5 + field public static final int FINGERPRINT_ERROR_HW_NOT_PRESENT = 12; // 0xc + field public static final int FINGERPRINT_ERROR_HW_UNAVAILABLE = 1; // 0x1 + field public static final int FINGERPRINT_ERROR_LOCKOUT = 7; // 0x7 + field public static final int FINGERPRINT_ERROR_LOCKOUT_PERMANENT = 9; // 0x9 + field public static final int FINGERPRINT_ERROR_NO_FINGERPRINTS = 11; // 0xb + field public static final int FINGERPRINT_ERROR_NO_SPACE = 4; // 0x4 + field public static final int FINGERPRINT_ERROR_TIMEOUT = 3; // 0x3 + field public static final int FINGERPRINT_ERROR_UNABLE_TO_PROCESS = 2; // 0x2 + field public static final int FINGERPRINT_ERROR_USER_CANCELED = 10; // 0xa + field public static final int FINGERPRINT_ERROR_VENDOR = 8; // 0x8 + } + + @Deprecated public abstract static class FingerprintManager.AuthenticationCallback { + ctor @Deprecated public FingerprintManager.AuthenticationCallback(); + method @Deprecated public void onAuthenticationError(int, CharSequence); + method @Deprecated public void onAuthenticationFailed(); + method @Deprecated public void onAuthenticationHelp(int, CharSequence); + method @Deprecated public void onAuthenticationSucceeded(android.hardware.fingerprint.FingerprintManager.AuthenticationResult); + } + + @Deprecated public static class FingerprintManager.AuthenticationResult { + method @Deprecated public android.hardware.fingerprint.FingerprintManager.CryptoObject getCryptoObject(); + } + + @Deprecated public static final class FingerprintManager.CryptoObject { + ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull java.security.Signature); + ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull javax.crypto.Cipher); + ctor @Deprecated public FingerprintManager.CryptoObject(@NonNull javax.crypto.Mac); + method @Deprecated public javax.crypto.Cipher getCipher(); + method @Deprecated public javax.crypto.Mac getMac(); + method @Deprecated public java.security.Signature getSignature(); + } + +} + package android.hardware.input { public final class HostUsiVersion implements android.os.Parcelable { diff --git a/core/api/removed.txt b/core/api/removed.txt index c61f16333fe8..3c7c0d6e6ea1 100644 --- a/core/api/removed.txt +++ b/core/api/removed.txt @@ -35,7 +35,6 @@ package android.content { method @Deprecated @Nullable public String getFeatureId(); method public abstract android.content.SharedPreferences getSharedPreferences(java.io.File, int); method public abstract java.io.File getSharedPreferencesPath(String); - field public static final String FINGERPRINT_SERVICE = "fingerprint"; } public class ContextWrapper extends android.content.Context { @@ -146,54 +145,6 @@ package android.hardware { } -package android.hardware.fingerprint { - - @Deprecated public class FingerprintManager { - method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.USE_BIOMETRIC, android.Manifest.permission.USE_FINGERPRINT}) public void authenticate(@Nullable android.hardware.fingerprint.FingerprintManager.CryptoObject, @Nullable android.os.CancellationSignal, int, @NonNull android.hardware.fingerprint.FingerprintManager.AuthenticationCallback, @Nullable android.os.Handler); - method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean hasEnrolledFingerprints(); - method @Deprecated @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public boolean isHardwareDetected(); - field public static final int FINGERPRINT_ACQUIRED_GOOD = 0; // 0x0 - field public static final int FINGERPRINT_ACQUIRED_IMAGER_DIRTY = 3; // 0x3 - field public static final int FINGERPRINT_ACQUIRED_INSUFFICIENT = 2; // 0x2 - field public static final int FINGERPRINT_ACQUIRED_PARTIAL = 1; // 0x1 - field public static final int FINGERPRINT_ACQUIRED_TOO_FAST = 5; // 0x5 - field public static final int FINGERPRINT_ACQUIRED_TOO_SLOW = 4; // 0x4 - field public static final int FINGERPRINT_ERROR_CANCELED = 5; // 0x5 - field public static final int FINGERPRINT_ERROR_HW_NOT_PRESENT = 12; // 0xc - field public static final int FINGERPRINT_ERROR_HW_UNAVAILABLE = 1; // 0x1 - field public static final int FINGERPRINT_ERROR_LOCKOUT = 7; // 0x7 - field public static final int FINGERPRINT_ERROR_LOCKOUT_PERMANENT = 9; // 0x9 - field public static final int FINGERPRINT_ERROR_NO_FINGERPRINTS = 11; // 0xb - field public static final int FINGERPRINT_ERROR_NO_SPACE = 4; // 0x4 - field public static final int FINGERPRINT_ERROR_TIMEOUT = 3; // 0x3 - field public static final int FINGERPRINT_ERROR_UNABLE_TO_PROCESS = 2; // 0x2 - field public static final int FINGERPRINT_ERROR_USER_CANCELED = 10; // 0xa - field public static final int FINGERPRINT_ERROR_VENDOR = 8; // 0x8 - } - - @Deprecated public abstract static class FingerprintManager.AuthenticationCallback { - ctor public FingerprintManager.AuthenticationCallback(); - method public void onAuthenticationError(int, CharSequence); - method public void onAuthenticationFailed(); - method public void onAuthenticationHelp(int, CharSequence); - method public void onAuthenticationSucceeded(android.hardware.fingerprint.FingerprintManager.AuthenticationResult); - } - - @Deprecated public static class FingerprintManager.AuthenticationResult { - method public android.hardware.fingerprint.FingerprintManager.CryptoObject getCryptoObject(); - } - - @Deprecated public static final class FingerprintManager.CryptoObject { - ctor public FingerprintManager.CryptoObject(@NonNull java.security.Signature); - ctor public FingerprintManager.CryptoObject(@NonNull javax.crypto.Cipher); - ctor public FingerprintManager.CryptoObject(@NonNull javax.crypto.Mac); - method public javax.crypto.Cipher getCipher(); - method public javax.crypto.Mac getMac(); - method public java.security.Signature getSignature(); - } - -} - package android.media { public final class AudioFormat implements android.os.Parcelable { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8ceda62e0e02..22d39a4a0fa6 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -598,7 +598,6 @@ package android.app { field public static final int FOREGROUND_SERVICE_API_TYPE_MICROPHONE = 6; // 0x6 field public static final int FOREGROUND_SERVICE_API_TYPE_PHONE_CALL = 7; // 0x7 field public static final int FOREGROUND_SERVICE_API_TYPE_USB = 8; // 0x8 - field @FlaggedApi("android.media.audio.foreground_audio_control") public static final int PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL = 64; // 0x40 field public static final int PROCESS_CAPABILITY_FOREGROUND_CAMERA = 2; // 0x2 field public static final int PROCESS_CAPABILITY_FOREGROUND_LOCATION = 1; // 0x1 field public static final int PROCESS_CAPABILITY_FOREGROUND_MICROPHONE = 4; // 0x4 @@ -3797,7 +3796,6 @@ package android.content { field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String ON_DEVICE_INTELLIGENCE_SERVICE = "on_device_intelligence"; field public static final String PERMISSION_CONTROLLER_SERVICE = "permission_controller"; field public static final String PERMISSION_SERVICE = "permission"; - field public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; field public static final String REBOOT_READINESS_SERVICE = "reboot_readiness"; field public static final String ROLLBACK_SERVICE = "rollback"; field public static final String SAFETY_CENTER_SERVICE = "safety_center"; @@ -4356,7 +4354,7 @@ package android.content.pm { field @Deprecated public static final int INTENT_FILTER_VERIFICATION_SUCCESS = 1; // 0x1 field @Deprecated public static final int MASK_PERMISSION_FLAGS = 255; // 0xff field public static final int MATCH_ANY_USER = 4194304; // 0x400000 - field public static final int MATCH_CLONE_PROFILE = 536870912; // 0x20000000 + field @Deprecated public static final int MATCH_CLONE_PROFILE = 536870912; // 0x20000000 field @FlaggedApi("android.content.pm.fix_duplicated_flags") public static final long MATCH_CLONE_PROFILE_LONG = 17179869184L; // 0x400000000L field public static final int MATCH_FACTORY_ONLY = 2097152; // 0x200000 field public static final int MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS = 536870912; // 0x20000000 @@ -15344,7 +15342,7 @@ package android.telephony { method @Deprecated public boolean getDataEnabled(int); method @Nullable @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.ComponentName getDefaultRespondViaMessageApplication(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getDeviceSoftwareVersion(int); - method @FlaggedApi("android.permission.flags.get_emergency_role_holder_api_enabled") @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getEmergencyAssistancePackageName(); + method @FlaggedApi("android.permission.flags.get_emergency_role_holder_api_enabled") @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getEmergencyAssistancePackageName(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean getEmergencyCallbackMode(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEmergencyNumberDbVersion(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimDomain(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 0a26490b772f..a76aa6743bc5 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1721,6 +1721,15 @@ package android.hardware.display { } +package android.hardware.fingerprint { + + @Deprecated public class FingerprintManager { + method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public android.hardware.biometrics.BiometricTestSession createTestSession(int); + method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public java.util.List<android.hardware.biometrics.SensorProperties> getSensorProperties(); + } + +} + package android.hardware.hdmi { public final class HdmiControlServiceWrapper { @@ -2460,6 +2469,7 @@ package android.os { } public class UserManager { + method @FlaggedApi("android.os.allow_private_profile") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional=true) public boolean canAddPrivateProfile(); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createProfileForUser(@Nullable String, @NonNull String, int, int, @Nullable String[]); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createRestrictedProfile(@Nullable String); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createUser(@Nullable String, @NonNull String, int); diff --git a/core/api/test-removed.txt b/core/api/test-removed.txt index 2e44176f342e..d802177e249b 100644 --- a/core/api/test-removed.txt +++ b/core/api/test-removed.txt @@ -1,10 +1 @@ // Signature format: 2.0 -package android.hardware.fingerprint { - - @Deprecated public class FingerprintManager { - method @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public android.hardware.biometrics.BiometricTestSession createTestSession(int); - method @NonNull @RequiresPermission(android.Manifest.permission.TEST_BIOMETRIC) public java.util.List<android.hardware.biometrics.SensorProperties> getSensorProperties(); - } - -} - diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 1cc2d25fb76d..a5dd4a7207c3 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -796,7 +796,7 @@ public class Activity extends ContextThemeWrapper private static final String SAVED_DIALOGS_TAG = "android:savedDialogs"; private static final String SAVED_DIALOG_KEY_PREFIX = "android:dialog_"; private static final String SAVED_DIALOG_ARGS_KEY_PREFIX = "android:dialog_args_"; - private static final String HAS_CURENT_PERMISSIONS_REQUEST_KEY = + private static final String HAS_CURRENT_PERMISSIONS_REQUEST_KEY = "android:hasCurrentPermissionsRequest"; private static final String REQUEST_PERMISSIONS_WHO_PREFIX = "@android:requestPermissions:"; @@ -9318,14 +9318,14 @@ public class Activity extends ContextThemeWrapper private void storeHasCurrentPermissionRequest(Bundle bundle) { if (bundle != null && mHasCurrentPermissionsRequest) { - bundle.putBoolean(HAS_CURENT_PERMISSIONS_REQUEST_KEY, true); + bundle.putBoolean(HAS_CURRENT_PERMISSIONS_REQUEST_KEY, true); } } private void restoreHasCurrentPermissionRequest(Bundle bundle) { if (bundle != null) { mHasCurrentPermissionsRequest = bundle.getBoolean( - HAS_CURENT_PERMISSIONS_REQUEST_KEY, false); + HAS_CURRENT_PERMISSIONS_REQUEST_KEY, false); } } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index fae434828222..0c543515f4cf 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -20,7 +20,6 @@ import static android.app.WindowConfiguration.activityTypeToString; import static android.app.WindowConfiguration.windowingModeToString; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; -import static android.media.audio.Flags.FLAG_FOREGROUND_AUDIO_CONTROL; import android.Manifest; import android.annotation.ColorInt; @@ -948,8 +947,6 @@ public class ActivityManager { * @hide * Process can access volume APIs and can request audio focus with GAIN. */ - @FlaggedApi(FLAG_FOREGROUND_AUDIO_CONTROL) - @SystemApi public static final int PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL = 1 << 6; /** diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index ae5cacd18aa2..fa9346e89a9f 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -712,16 +712,22 @@ public final class ActivityThread extends ClientTransactionHandler stopped = false; hideForNow = false; activityConfigCallback = new ViewRootImpl.ActivityConfigCallback() { + @Override - public void onConfigurationChanged(Configuration overrideConfig, - int newDisplayId) { + public void onConfigurationChanged(@NonNull Configuration overrideConfig, + int newDisplayId, @Nullable ActivityWindowInfo activityWindowInfo) { if (activity == null) { throw new IllegalStateException( "Received config update for non-existing activity"); } + if (activityWindowInfoFlag() && activityWindowInfo == null) { + Log.w(TAG, "Received empty ActivityWindowInfo update for r=" + activity); + activityWindowInfo = mActivityWindowInfo; + } activity.mMainThread.handleActivityConfigurationChanged( ActivityClientRecord.this, overrideConfig, newDisplayId, - mActivityWindowInfo, false /* alwaysReportChange */); + activityWindowInfo, + false /* alwaysReportChange */); } @Override diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index a8352fad8a90..0ed25eb3125a 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1581,6 +1581,10 @@ public class AppOpsManager { * Allows an app to access location without the traditional location permissions and while the * user location setting is off, but only during pre-defined emergency sessions. * + * <p>This op is only used for tracking, not for permissions, so it is still the client's + * responsibility to check the {@link Manifest.permission.LOCATION_BYPASS} permission + * appropriately. + * * @hide */ public static final int OP_EMERGENCY_LOCATION = AppProtoEnums.APP_OP_EMERGENCY_LOCATION; @@ -2459,6 +2463,10 @@ public class AppOpsManager { * Allows an app to access location without the traditional location permissions and while the * user location setting is off, but only during pre-defined emergency sessions. * + * <p>This op is only used for tracking, not for permissions, so it is still the client's + * responsibility to check the {@link Manifest.permission.LOCATION_BYPASS} permission + * appropriately. + * * @hide */ @SystemApi @@ -2677,8 +2685,7 @@ public class AppOpsManager { .setDefaultMode(getSystemAlertWindowDefault()).build(), new AppOpInfo.Builder(OP_ACCESS_NOTIFICATIONS, OPSTR_ACCESS_NOTIFICATIONS, "ACCESS_NOTIFICATIONS") - .setPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) - .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), + .setPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS).build(), new AppOpInfo.Builder(OP_CAMERA, OPSTR_CAMERA, "CAMERA") .setPermission(android.Manifest.permission.CAMERA) .setRestriction(UserManager.DISALLOW_CAMERA) @@ -3047,8 +3054,10 @@ public class AppOpsManager { new AppOpInfo.Builder(OP_UNARCHIVAL_CONFIRMATION, OPSTR_UNARCHIVAL_CONFIRMATION, "UNARCHIVAL_CONFIRMATION") .setDefaultMode(MODE_ALLOWED).build(), - // TODO(b/301150056): STOPSHIP determine how this appop should work with the permission new AppOpInfo.Builder(OP_EMERGENCY_LOCATION, OPSTR_EMERGENCY_LOCATION, "EMERGENCY_LOCATION") + .setDefaultMode(MODE_ALLOWED) + // even though this has a permission associated, this op is only used for tracking, + // and the client is responsible for checking the LOCATION_BYPASS permission. .setPermission(Manifest.permission.LOCATION_BYPASS).build(), }; diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java index 24cb9ea87a12..cac10f588aa8 100644 --- a/core/java/android/app/ApplicationExitInfo.java +++ b/core/java/android/app/ApplicationExitInfo.java @@ -487,6 +487,15 @@ public final class ApplicationExitInfo implements Parcelable { */ public static final int SUBREASON_FREEZER_BINDER_ASYNC_FULL = 31; + /** + * The process was killed because it was sending too many broadcasts while it is in the + * Cached state. This would be set only when the reason is {@link #REASON_OTHER}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED = 32; + // If there is any OEM code which involves additional app kill reasons, it should // be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000. @@ -665,6 +674,7 @@ public final class ApplicationExitInfo implements Parcelable { SUBREASON_EXCESSIVE_BINDER_OBJECTS, SUBREASON_OOM_KILL, SUBREASON_FREEZER_BINDER_ASYNC_FULL, + SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED, }) @Retention(RetentionPolicy.SOURCE) public @interface SubReason {} @@ -1396,6 +1406,8 @@ public final class ApplicationExitInfo implements Parcelable { return "OOM KILL"; case SUBREASON_FREEZER_BINDER_ASYNC_FULL: return "FREEZER BINDER ASYNC FULL"; + case SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED: + return "EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED"; default: return "UNKNOWN"; } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 6f6e0911fa4b..716dee4dc082 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -344,23 +344,37 @@ class ContextImpl extends Context { */ private boolean mOwnsToken = false; - private final Object mDirsLock = new Object(); - @GuardedBy("mDirsLock") + private final Object mDatabasesDirLock = new Object(); + @GuardedBy("mDatabasesDirLock") private File mDatabasesDir; - @GuardedBy("mDirsLock") + + private final Object mPreferencesDirLock = new Object(); @UnsupportedAppUsage + @GuardedBy("mPreferencesDirLock") private File mPreferencesDir; - @GuardedBy("mDirsLock") + + private final Object mFilesDirLock = new Object(); + @GuardedBy("mFilesDirLock") private File mFilesDir; - @GuardedBy("mDirsLock") + + private final Object mCratesDirLock = new Object(); + @GuardedBy("mCratesDirLock") private File mCratesDir; - @GuardedBy("mDirsLock") + + private final Object mNoBackupFilesDirLock = new Object(); + @GuardedBy("mNoBackupFilesDirLock") private File mNoBackupFilesDir; - @GuardedBy("mDirsLock") + + private final Object mCacheDirLock = new Object(); + @GuardedBy("mCacheDirLock") private File mCacheDir; - @GuardedBy("mDirsLock") + + private final Object mCodeCacheDirLock = new Object(); + @GuardedBy("mCodeCacheDirLock") private File mCodeCacheDir; + private final Object mMiscDirsLock = new Object(); + // The system service cache for the system services that are cached per-ContextImpl. @UnsupportedAppUsage final Object[] mServiceCache = SystemServiceRegistry.createServiceCache(); @@ -742,7 +756,7 @@ class ContextImpl extends Context { @UnsupportedAppUsage private File getPreferencesDir() { - synchronized (mDirsLock) { + synchronized (mPreferencesDirLock) { if (mPreferencesDir == null) { mPreferencesDir = new File(getDataDir(), "shared_prefs"); } @@ -831,7 +845,7 @@ class ContextImpl extends Context { @Override public File getFilesDir() { - synchronized (mDirsLock) { + synchronized (mFilesDirLock) { if (mFilesDir == null) { mFilesDir = new File(getDataDir(), "files"); } @@ -846,7 +860,7 @@ class ContextImpl extends Context { final Path absoluteNormalizedCratePath = cratesRootPath.resolve(crateId) .toAbsolutePath().normalize(); - synchronized (mDirsLock) { + synchronized (mCratesDirLock) { if (mCratesDir == null) { mCratesDir = cratesRootPath.toFile(); } @@ -859,7 +873,7 @@ class ContextImpl extends Context { @Override public File getNoBackupFilesDir() { - synchronized (mDirsLock) { + synchronized (mNoBackupFilesDirLock) { if (mNoBackupFilesDir == null) { mNoBackupFilesDir = new File(getDataDir(), "no_backup"); } @@ -876,7 +890,7 @@ class ContextImpl extends Context { @Override public File[] getExternalFilesDirs(String type) { - synchronized (mDirsLock) { + synchronized (mMiscDirsLock) { File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName()); if (type != null) { dirs = Environment.buildPaths(dirs, type); @@ -894,7 +908,7 @@ class ContextImpl extends Context { @Override public File[] getObbDirs() { - synchronized (mDirsLock) { + synchronized (mMiscDirsLock) { File[] dirs = Environment.buildExternalStorageAppObbDirs(getPackageName()); return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */); } @@ -902,7 +916,7 @@ class ContextImpl extends Context { @Override public File getCacheDir() { - synchronized (mDirsLock) { + synchronized (mCacheDirLock) { if (mCacheDir == null) { mCacheDir = new File(getDataDir(), "cache"); } @@ -912,7 +926,7 @@ class ContextImpl extends Context { @Override public File getCodeCacheDir() { - synchronized (mDirsLock) { + synchronized (mCodeCacheDirLock) { if (mCodeCacheDir == null) { mCodeCacheDir = getCodeCacheDirBeforeBind(getDataDir()); } @@ -938,7 +952,7 @@ class ContextImpl extends Context { @Override public File[] getExternalCacheDirs() { - synchronized (mDirsLock) { + synchronized (mMiscDirsLock) { File[] dirs = Environment.buildExternalStorageAppCacheDirs(getPackageName()); // We don't try to create cache directories in-process, because they need special // setup for accurate quota tracking. This ensures the cache dirs are always @@ -949,7 +963,7 @@ class ContextImpl extends Context { @Override public File[] getExternalMediaDirs() { - synchronized (mDirsLock) { + synchronized (mMiscDirsLock) { File[] dirs = Environment.buildExternalStorageAppMediaDirs(getPackageName()); return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */); } @@ -1051,7 +1065,7 @@ class ContextImpl extends Context { } private File getDatabasesDir() { - synchronized (mDirsLock) { + synchronized (mDatabasesDirLock) { if (mDatabasesDir == null) { if ("android".equals(getPackageName())) { mDatabasesDir = new File("/data/system"); diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index fe8655c13562..f092945a5d28 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -1135,6 +1135,9 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac } catch (RemoteException ex) { } onTimeout(startId); + if (Flags.introduceNewServiceOntimeoutCallback()) { + onTimeout(startId, ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE); + } } /** @@ -1146,6 +1149,12 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac * doesn't finish even after it's timed out, * the app will be declared an ANR after a short grace period of several seconds. * + * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * {@link #onTimeout(int, int)} will also be called when a foreground service of type + * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE} times out. + * Developers do not need to implement both of the callbacks on + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} and onwards. + * * <p>Note, even though * {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_SHORT_SERVICE} * was added diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 1cbec3126aac..66ec865092f7 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -450,6 +450,11 @@ public final class SystemServiceRegistry { new CachedServiceFetcher<VcnManager>() { @Override public VcnManager createService(ContextImpl ctx) throws ServiceNotFoundException { + if (!ctx.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { + return null; + } + IBinder b = ServiceManager.getService(Context.VCN_MANAGEMENT_SERVICE); IVcnManagementService service = IVcnManagementService.Stub.asInterface(b); return new VcnManager(ctx, service); @@ -1736,6 +1741,13 @@ public final class SystemServiceRegistry { return fetcher; } + private static boolean hasSystemFeatureOpportunistic(@NonNull ContextImpl ctx, + @NonNull String featureName) { + PackageManager manager = ctx.getPackageManager(); + if (manager == null) return true; + return manager.hasSystemFeature(featureName); + } + /** * Gets a system service from a given context. * @hide @@ -1758,12 +1770,18 @@ public final class SystemServiceRegistry { case Context.VIRTUALIZATION_SERVICE: case Context.VIRTUAL_DEVICE_SERVICE: return null; + case Context.VCN_MANAGEMENT_SERVICE: + if (!hasSystemFeatureOpportunistic(ctx, + PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { + return null; + } + break; case Context.SEARCH_SERVICE: // Wear device does not support SEARCH_SERVICE so we do not print WTF here - PackageManager manager = ctx.getPackageManager(); - if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_WATCH)) { + if (hasSystemFeatureOpportunistic(ctx, PackageManager.FEATURE_WATCH)) { return null; } + break; } Slog.wtf(TAG, "Manager wrapper not available: " + name); return null; diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index 350b1edf2129..b9aa18c0211c 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -3,6 +3,7 @@ package: "android.app" flag { namespace: "system_performance" name: "app_start_info" + is_exported: true description: "Control collecting of ApplicationStartInfo records and APIs." bug: "247814855" } @@ -10,6 +11,7 @@ flag { flag { namespace: "backstage_power" name: "get_binding_uid_importance" + is_exported: true description: "API to get importance of UID that's binding to the caller" bug: "292533010" } @@ -17,6 +19,7 @@ flag { flag { namespace: "backstage_power" name: "app_restrictions_api" + is_exported: true description: "API to track and query restrictions applied to apps" bug: "320150834" } @@ -24,6 +27,7 @@ flag { flag { namespace: "backstage_power" name: "uid_importance_listener_for_uids" + is_exported: true description: "API to add OnUidImportanceListener with targetted UIDs" bug: "286258140" } @@ -31,12 +35,14 @@ flag { flag { namespace: "backstage_power" name: "introduce_new_service_ontimeout_callback" + is_exported: true description: "Add a new callback in Service to indicate a FGS has reached its timeout." bug: "317799821" } flag { name: "bcast_event_timestamps" + is_exported: true namespace: "backstage_power" description: "Add APIs for clients to provide broadcast event trigger timestamps" bug: "325136414" diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a075ac51e1ed..60dffbd0e421 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -6545,8 +6545,10 @@ public class DevicePolicyManager { } /** - * Flag for {@link #wipeData(int)}: also erase the device's external - * storage (such as SD cards). + * Flag for {@link #wipeData(int)}: also erase the device's adopted external storage (such as + * adopted SD cards). + * @see <a href="{@docRoot}about/versions/marshmallow/android-6.0.html#adoptable-storage"> + * Adoptable Storage Devices</a> */ public static final int WIPE_EXTERNAL_STORAGE = 0x0001; diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 441d52148b7b..4fa45be57a11 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -1,7 +1,11 @@ +# proto-file: build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto +# proto-message: flag_declarations + package: "android.app.admin.flags" flag { name: "policy_engine_migration_v2_enabled" + is_exported: true namespace: "enterprise" description: "V2 of the policy engine migrations for Android V" bug: "289520697" @@ -9,6 +13,7 @@ flag { flag { name: "device_policy_size_tracking_enabled" + is_exported: true namespace: "enterprise" description: "Add feature to track the total policy size and have a max threshold - public API changes" bug: "281543351" @@ -23,6 +28,7 @@ flag { flag { name: "onboarding_bugreport_v2_enabled" + is_exported: true namespace: "enterprise" description: "Add feature to track required changes for enabled V2 of auto-capturing of onboarding bug reports." bug: "302517677" @@ -44,6 +50,7 @@ flag { flag { name: "dedicated_device_control_api_enabled" + is_exported: true namespace: "enterprise" description: "(API) Allow the device management role holder to control which platform features are available on dedicated devices." bug: "281964214" @@ -51,6 +58,7 @@ flag { flag { name: "permission_migration_for_zero_trust_api_enabled" + is_exported: true namespace: "enterprise" description: "(API) Migrate existing APIs to permission based, and enable DMRH to call them to collect Zero Trust signals." bug: "289520697" @@ -65,6 +73,7 @@ flag { flag { name: "device_theft_api_enabled" + is_exported: true namespace: "enterprise" description: "Add new API for theft detection." bug: "325073410" @@ -86,6 +95,7 @@ flag { flag { name: "security_log_v2_enabled" + is_exported: true namespace: "enterprise" description: "Improve access to security logging in the context of Zero Trust." bug: "295324350" @@ -100,6 +110,7 @@ flag { flag { name: "allow_querying_profile_type" + is_exported: true namespace: "enterprise" description: "Public APIs to query if a user is a profile and what kind of profile type it is." bug: "323001115" @@ -114,6 +125,7 @@ flag { flag { name: "assist_content_user_restriction_enabled" + is_exported: true namespace: "enterprise" description: "Prevent work data leakage by sending assist content to privileged apps." bug: "322975406" @@ -131,6 +143,7 @@ flag { flag { name: "backup_service_security_log_event_enabled" + is_exported: true namespace: "enterprise" description: "Emit a security log event when DPM.setBackupServiceEnabled is called" bug: "304999634" @@ -138,6 +151,7 @@ flag { flag { name: "esim_management_enabled" + is_exported: true namespace: "enterprise" description: "Enable APIs to provision and manage eSIMs" bug: "295301164" @@ -145,6 +159,7 @@ flag { flag { name: "headless_device_owner_single_user_enabled" + is_exported: true namespace: "enterprise" description: "Add Headless DO support." bug: "289515470" @@ -152,6 +167,7 @@ flag { flag { name: "is_mte_policy_enforced" + is_exported: true namespace: "enterprise" description: "Allow to query whether MTE is enabled or not to check for compliance for enterprise policy" bug: "322777918" @@ -180,3 +196,10 @@ flag { description: "Allow COPE admin to control screen brightness and timeout." bug: "323894620" } + +flag { + name: "is_recursive_required_app_merging_enabled" + namespace: "enterprise" + description: "Guards a new flow for recursive required enterprise app list merging" + bug: "319084618" +} diff --git a/core/java/android/app/background_install_control_manager.aconfig b/core/java/android/app/background_install_control_manager.aconfig index 4473b9523f1b..5f3bb0745b08 100644 --- a/core/java/android/app/background_install_control_manager.aconfig +++ b/core/java/android/app/background_install_control_manager.aconfig @@ -3,6 +3,7 @@ package: "android.app" flag { namespace: "preload_safety" name: "bic_client" + is_exported: true description: "System API for background install control." is_fixed_read_only: true bug: "287507984" diff --git a/core/java/android/app/grammatical_inflection_manager.aconfig b/core/java/android/app/grammatical_inflection_manager.aconfig index 68d12ba75560..0d7bf65215a0 100644 --- a/core/java/android/app/grammatical_inflection_manager.aconfig +++ b/core/java/android/app/grammatical_inflection_manager.aconfig @@ -2,6 +2,7 @@ package: "android.app" flag { name: "system_terms_of_address_enabled" + is_exported: true namespace: "globalintl" description: "Feature flag for System Terms of Address" bug: "297798866" diff --git a/core/java/android/app/multitasking.aconfig b/core/java/android/app/multitasking.aconfig index ab00891b9b31..dbf3173a4ee6 100644 --- a/core/java/android/app/multitasking.aconfig +++ b/core/java/android/app/multitasking.aconfig @@ -2,6 +2,7 @@ package: "android.app" flag { name: "enable_pip_ui_state_callback_on_entering" + is_exported: true namespace: "multitasking" description: "Enables PiP UI state callback on entering" bug: "303718131" diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 274d02a79270..e9a746022a75 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -2,6 +2,7 @@ package: "android.app" flag { name: "modes_api" + is_exported: true namespace: "systemui" description: "This flag controls new and updated DND apis" bug: "300477976" @@ -16,6 +17,7 @@ flag { flag { name: "api_tvextender" + is_exported: true namespace: "systemui" description: "Guards new android.app.Notification.TvExtender api" bug: "308164892" @@ -24,6 +26,7 @@ flag { flag { name: "lifetime_extension_refactor" + is_exported: true namespace: "systemui" description: "Enables moving notification lifetime extension management from SystemUI to " "Notification Manager Service" @@ -46,6 +49,7 @@ flag { flag { name: "category_voicemail" + is_exported: true namespace: "wear_sysui" description: "Adds a new voicemail category for notifications" bug: "322806700" @@ -53,6 +57,7 @@ flag { flag { name: "notification_channel_vibration_effect_api" + is_exported: true namespace: "systemui" description: "This flag enables the API to allow setting VibrationEffect for NotificationChannels" bug: "241732519" diff --git a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl index 0dbe18156904..8bf288abb0f9 100644 --- a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl +++ b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl @@ -53,19 +53,22 @@ void getFeatureDetails(in Feature feature, in IFeatureDetailsCallback featureDetailsCallback) = 4; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void requestFeatureDownload(in Feature feature, in ICancellationSignal signal, in IDownloadCallback callback) = 5; + void requestFeatureDownload(in Feature feature, in AndroidFuture cancellationSignalFuture, in IDownloadCallback callback) = 5; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void requestTokenInfo(in Feature feature, in Bundle requestBundle, in ICancellationSignal signal, + void requestTokenInfo(in Feature feature, in Bundle requestBundle, in AndroidFuture cancellationSignalFuture, in ITokenInfoCallback tokenInfocallback) = 6; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void processRequest(in Feature feature, in Bundle requestBundle, int requestType, in ICancellationSignal cancellationSignal, - in IProcessingSignal signal, in IResponseCallback responseCallback) = 7; + void processRequest(in Feature feature, in Bundle requestBundle, int requestType, + in AndroidFuture cancellationSignalFuture, + in AndroidFuture processingSignalFuture, + in IResponseCallback responseCallback) = 7; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") void processRequestStreaming(in Feature feature, - in Bundle requestBundle, int requestType, in ICancellationSignal cancellationSignal, in IProcessingSignal signal, + in Bundle requestBundle, int requestType, in AndroidFuture cancellationSignalFuture, + in AndroidFuture processingSignalFuture, in IStreamingResponseCallback streamingCallback) = 8; String getRemoteServicePackageName() = 9; diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java index a465e3cbb6ec..bc50d2e492ae 100644 --- a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java +++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java @@ -26,22 +26,23 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; -import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; import android.os.Binder; import android.os.Bundle; import android.os.CancellationSignal; +import android.os.IBinder; import android.os.ICancellationSignal; import android.os.OutcomeReceiver; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.RemoteException; import android.system.OsConstants; +import android.util.Log; import androidx.annotation.IntDef; -import com.android.internal.R; +import com.android.internal.infra.AndroidFuture; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -76,6 +77,8 @@ public final class OnDeviceIntelligenceManager { */ public static final String AUGMENT_REQUEST_CONTENT_BUNDLE_KEY = "AugmentRequestContentBundleKey"; + + private static final String TAG = "OnDeviceIntelligence"; private final Context mContext; private final IOnDeviceIntelligenceManager mService; @@ -121,9 +124,9 @@ public final class OnDeviceIntelligenceManager { @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public String getRemoteServicePackageName() { String result; - try{ - result = mService.getRemoteServicePackageName(); - } catch (RemoteException e){ + try { + result = mService.getRemoteServicePackageName(); + } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return result; @@ -288,18 +291,15 @@ public final class OnDeviceIntelligenceManager { } }; - ICancellationSignal transport = null; - if (cancellationSignal != null) { - transport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(transport); - } - - mService.requestFeatureDownload(feature, transport, downloadCallback); + mService.requestFeatureDownload(feature, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + downloadCallback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + /** * The methods computes the token related information for a given request payload using the * provided {@link Feature}. @@ -337,13 +337,9 @@ public final class OnDeviceIntelligenceManager { } }; - ICancellationSignal transport = null; - if (cancellationSignal != null) { - transport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(transport); - } - - mService.requestTokenInfo(feature, request, transport, callback); + mService.requestTokenInfo(feature, request, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -407,19 +403,9 @@ public final class OnDeviceIntelligenceManager { }; - IProcessingSignal transport = null; - if (processingSignal != null) { - transport = ProcessingSignal.createTransport(); - processingSignal.setRemote(transport); - } - - ICancellationSignal cancellationTransport = null; - if (cancellationSignal != null) { - cancellationTransport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(cancellationTransport); - } - - mService.processRequest(feature, request, requestType, cancellationTransport, transport, + mService.processRequest(feature, request, requestType, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor), callback); } catch (RemoteException e) { @@ -449,7 +435,8 @@ public final class OnDeviceIntelligenceManager { * @param callbackExecutor executor to run the callback on. */ @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) - public void processRequestStreaming(@NonNull Feature feature, @NonNull @InferenceParams Bundle request, + public void processRequestStreaming(@NonNull Feature feature, + @NonNull @InferenceParams Bundle request, @RequestType int requestType, @Nullable CancellationSignal cancellationSignal, @Nullable ProcessingSignal processingSignal, @@ -500,20 +487,11 @@ public final class OnDeviceIntelligenceManager { } }; - IProcessingSignal transport = null; - if (processingSignal != null) { - transport = ProcessingSignal.createTransport(); - processingSignal.setRemote(transport); - } - - ICancellationSignal cancellationTransport = null; - if (cancellationSignal != null) { - cancellationTransport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(cancellationTransport); - } - mService.processRequestStreaming( - feature, request, requestType, cancellationTransport, transport, callback); + feature, request, requestType, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor), + callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -574,4 +552,45 @@ public final class OnDeviceIntelligenceManager { @Target({ElementType.PARAMETER, ElementType.FIELD}) public @interface InferenceParams { } + + + @Nullable + private static AndroidFuture<IBinder> configureRemoteCancellationFuture( + @Nullable CancellationSignal cancellationSignal, + @NonNull Executor callbackExecutor) { + if (cancellationSignal == null) { + return null; + } + AndroidFuture<IBinder> cancellationFuture = new AndroidFuture<>(); + cancellationFuture.whenCompleteAsync( + (cancellationTransport, error) -> { + if (error != null || cancellationTransport == null) { + Log.e(TAG, "Unable to receive the remote cancellation signal.", error); + } else { + cancellationSignal.setRemote( + ICancellationSignal.Stub.asInterface(cancellationTransport)); + } + }, callbackExecutor); + return cancellationFuture; + } + + @Nullable + private static AndroidFuture<IBinder> configureRemoteProcessingSignalFuture( + ProcessingSignal processingSignal, Executor executor) { + if (processingSignal == null) { + return null; + } + AndroidFuture<IBinder> processingSignalFuture = new AndroidFuture<>(); + processingSignalFuture.whenCompleteAsync( + (transport, error) -> { + if (error != null || transport == null) { + Log.e(TAG, "Unable to receive the remote processing signal.", error); + } else { + processingSignal.setRemote(IProcessingSignal.Stub.asInterface(transport)); + } + }, executor); + return processingSignalFuture; + } + + } diff --git a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java index c275cc786007..733f4fad96f4 100644 --- a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java +++ b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java @@ -123,10 +123,10 @@ public final class ProcessingSignal { * Sets the processing signal callback to be called when signals are received. * * This method is intended to be used by the recipient of a processing signal - * such as the remote implementation for {@link OnDeviceIntelligenceManager} to handle - * cancellation requests while performing a long-running operation. This method is not - * intended - * to be used by applications themselves. + * such as the remote implementation in + * {@link android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService} to handle + * processing signals while performing a long-running operation. This method is not + * intended to be used by the caller themselves. * * If {@link ProcessingSignal#sendSignal} has already been called, then the provided callback * is invoked immediately and all previously queued actions are passed to remote signal. @@ -200,7 +200,7 @@ public final class ProcessingSignal { } /** - * Given a locally created transport, returns its associated cancellation signal. + * Given a locally created transport, returns its associated processing signal. * * @param transport The locally created transport, or null if none. * @return The associated processing signal, or null if none. diff --git a/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig b/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig index 44f33298b1b2..dd9210faa10c 100644 --- a/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig +++ b/core/java/android/app/ondeviceintelligence/flags/ondevice_intelligence.aconfig @@ -2,6 +2,7 @@ package: "android.app.ondeviceintelligence.flags" flag { name: "enable_on_device_intelligence" + is_exported: true namespace: "ondeviceintelligence" description: "Make methods on OnDeviceIntelligenceManager available for local inference." bug: "304755128" diff --git a/core/java/android/app/pinner-client.aconfig b/core/java/android/app/pinner-client.aconfig index b60ad9ee1f8d..0f7fa14d9b6a 100644 --- a/core/java/android/app/pinner-client.aconfig +++ b/core/java/android/app/pinner-client.aconfig @@ -3,6 +3,7 @@ package: "android.app" flag { namespace: "system_performance" name: "pinner_service_client_api" + is_exported: true description: "Control exposing PinnerService APIs." bug: "307594624" }
\ No newline at end of file diff --git a/core/java/android/app/servertransaction/WindowStateResizeItem.java b/core/java/android/app/servertransaction/WindowStateResizeItem.java index fedffe134ce1..1817c5eefb14 100644 --- a/core/java/android/app/servertransaction/WindowStateResizeItem.java +++ b/core/java/android/app/servertransaction/WindowStateResizeItem.java @@ -25,6 +25,7 @@ import android.annotation.Nullable; import android.app.ActivityThread; import android.app.ClientTransactionHandler; import android.content.Context; +import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.Trace; @@ -32,6 +33,7 @@ import android.util.Log; import android.util.MergedConfiguration; import android.view.IWindow; import android.view.InsetsState; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import java.util.Objects; @@ -55,6 +57,14 @@ public class WindowStateResizeItem extends ClientTransactionItem { private int mSyncSeqId; private boolean mDragResizing; + /** {@code null} if this is not an Activity window. */ + @Nullable + private IBinder mActivityToken; + + /** {@code null} if this is not an Activity window. */ + @Nullable + private ActivityWindowInfo mActivityWindowInfo; + @Override public void execute(@NonNull ClientTransactionHandler client, @NonNull PendingTransactionActions pendingActions) { @@ -65,7 +75,8 @@ public class WindowStateResizeItem extends ClientTransactionItem { } try { mWindow.resized(mFrames, mReportDraw, mConfiguration, mInsetsState, mForceLayout, - mAlwaysConsumeSystemBars, mDisplayId, mSyncSeqId, mDragResizing); + mAlwaysConsumeSystemBars, mDisplayId, mSyncSeqId, mDragResizing, + mActivityWindowInfo); } catch (RemoteException e) { // Should be a local call. // An exception could happen if the process is restarted. It is safe to ignore since @@ -78,6 +89,7 @@ public class WindowStateResizeItem extends ClientTransactionItem { @Nullable @Override public Context getContextToUpdate(@NonNull ClientTransactionHandler client) { + // TODO(b/260873529): dispatch for mActivityToken as well. // WindowStateResizeItem may update the global config with #mConfiguration. return ActivityThread.currentApplication(); } @@ -91,7 +103,8 @@ public class WindowStateResizeItem extends ClientTransactionItem { @NonNull ClientWindowFrames frames, boolean reportDraw, @NonNull MergedConfiguration configuration, @NonNull InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, - boolean dragResizing) { + boolean dragResizing, @Nullable IBinder activityToken, + @Nullable ActivityWindowInfo activityWindowInfo) { WindowStateResizeItem instance = ObjectPool.obtain(WindowStateResizeItem.class); if (instance == null) { @@ -107,6 +120,10 @@ public class WindowStateResizeItem extends ClientTransactionItem { instance.mDisplayId = displayId; instance.mSyncSeqId = syncSeqId; instance.mDragResizing = dragResizing; + instance.mActivityToken = activityToken; + instance.mActivityWindowInfo = activityWindowInfo != null + ? new ActivityWindowInfo(activityWindowInfo) + : null; return instance; } @@ -123,6 +140,8 @@ public class WindowStateResizeItem extends ClientTransactionItem { mDisplayId = INVALID_DISPLAY; mSyncSeqId = -1; mDragResizing = false; + mActivityToken = null; + mActivityWindowInfo = null; ObjectPool.recycle(this); } @@ -141,6 +160,8 @@ public class WindowStateResizeItem extends ClientTransactionItem { dest.writeInt(mDisplayId); dest.writeInt(mSyncSeqId); dest.writeBoolean(mDragResizing); + dest.writeStrongBinder(mActivityToken); + dest.writeTypedObject(mActivityWindowInfo, flags); } /** Reads from Parcel. */ @@ -155,6 +176,8 @@ public class WindowStateResizeItem extends ClientTransactionItem { mDisplayId = in.readInt(); mSyncSeqId = in.readInt(); mDragResizing = in.readBoolean(); + mActivityToken = in.readStrongBinder(); + mActivityWindowInfo = in.readTypedObject(ActivityWindowInfo.CREATOR); } public static final @NonNull Creator<WindowStateResizeItem> CREATOR = new Creator<>() { @@ -185,7 +208,9 @@ public class WindowStateResizeItem extends ClientTransactionItem { && mAlwaysConsumeSystemBars == other.mAlwaysConsumeSystemBars && mDisplayId == other.mDisplayId && mSyncSeqId == other.mSyncSeqId - && mDragResizing == other.mDragResizing; + && mDragResizing == other.mDragResizing + && Objects.equals(mActivityToken, other.mActivityToken) + && Objects.equals(mActivityWindowInfo, other.mActivityWindowInfo); } @Override @@ -201,6 +226,8 @@ public class WindowStateResizeItem extends ClientTransactionItem { result = 31 * result + mDisplayId; result = 31 * result + mSyncSeqId; result = 31 * result + (mDragResizing ? 1 : 0); + result = 31 * result + Objects.hashCode(mActivityToken); + result = 31 * result + Objects.hashCode(mActivityWindowInfo); return result; } @@ -209,6 +236,8 @@ public class WindowStateResizeItem extends ClientTransactionItem { return "WindowStateResizeItem{window=" + mWindow + ", reportDrawn=" + mReportDraw + ", configuration=" + mConfiguration + + ", activityToken=" + mActivityToken + + ", activityWindowInfo=" + mActivityWindowInfo + "}"; } diff --git a/core/java/android/app/smartspace/flags.aconfig b/core/java/android/app/smartspace/flags.aconfig index 12af888bfaa5..e90ba67fe6dd 100644 --- a/core/java/android/app/smartspace/flags.aconfig +++ b/core/java/android/app/smartspace/flags.aconfig @@ -2,6 +2,7 @@ package: "android.app.smartspace.flags" flag { name: "remote_views" + is_exported: true namespace: "sysui_integrations" description: "Flag to enable the FlaggedApi to include RemoteViews in SmartspaceTarget" bug: "300157758" @@ -9,6 +10,7 @@ flag { flag { name: "access_smartspace" + is_exported: true namespace: "sysui_integrations" description: "Flag to enable the ACCESS_SMARTSPACE check in SmartspaceManagerService" bug: "297207196" diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig index 4d9d911ed563..9a2d2e5d8319 100644 --- a/core/java/android/app/usage/flags.aconfig +++ b/core/java/android/app/usage/flags.aconfig @@ -2,6 +2,7 @@ package: "android.app.usage" flag { name: "user_interaction_type_api" + is_exported: true namespace: "backstage_power" description: "Feature flag for user interaction event report/query API" bug: "296061232" @@ -9,6 +10,7 @@ flag { flag { name: "report_usage_stats_permission" + is_exported: true namespace: "backstage_power" description: "Feature flag for the new REPORT_USAGE_STATS permission." bug: "296056771" @@ -31,6 +33,7 @@ flag { flag { name: "filter_based_event_query_api" + is_exported: true namespace: "backstage_power" description: " Feature flag to support filter based event query API" bug: "194321117" @@ -38,6 +41,7 @@ flag { flag { name: "get_app_bytes_by_data_type_api" + is_exported: true namespace: "system_performance" description: "Feature flag for collecting app data size by file type API" bug: "294088945" diff --git a/core/java/android/app/wearable/flags.aconfig b/core/java/android/app/wearable/flags.aconfig index b4f628ffc9b3..d1d7b5d85e2d 100644 --- a/core/java/android/app/wearable/flags.aconfig +++ b/core/java/android/app/wearable/flags.aconfig @@ -2,6 +2,7 @@ package: "android.app.wearable" flag { name: "enable_unsupported_operation_status_code" + is_exported: true namespace: "machine_learning" description: "This flag enables the WearableSensingManager#STATUS_UNSUPPORTED_OPERATION status code API." bug: "301427767" @@ -9,6 +10,7 @@ flag { flag { name: "enable_data_request_observer_api" + is_exported: true namespace: "machine_learning" description: "This flag enables the API to register a data request observer on WearableSensingManager." bug: "301427767" @@ -16,6 +18,7 @@ flag { flag { name: "enable_provide_wearable_connection_api" + is_exported: true namespace: "machine_learning" description: "This flag enables the WearableSensingManager#provideWearableConnection API." bug: "301427767" @@ -30,6 +33,7 @@ flag { flag { name: "enable_hotword_wearable_sensing_api" + is_exported: true namespace: "machine_learning" description: "This flag enables the APIs related to hotword in WearableSensingManager and WearableSensingService." bug: "310055381" diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index 2c0e035e80c4..57b5c13a659d 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -1384,7 +1384,8 @@ public class AppWidgetManager { * * @return {@code TRUE} if the launcher supports this feature. Note the API will return without * waiting for the user to respond, so getting {@code TRUE} from this API does *not* mean - * the shortcut is pinned. {@code FALSE} if the launcher doesn't support this feature. + * the shortcut is pinned. {@code FALSE} if the launcher doesn't support this feature or if + * calling app belongs to a user-profile with items restricted on home screen. * * @see android.content.pm.ShortcutManager#isRequestPinShortcutSupported() * @see android.content.pm.ShortcutManager#requestPinShortcut(ShortcutInfo, IntentSender) diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig index 822f02f70562..451195478760 100644 --- a/core/java/android/appwidget/flags.aconfig +++ b/core/java/android/appwidget/flags.aconfig @@ -2,6 +2,7 @@ package: "android.appwidget.flags" flag { name: "generated_previews" + is_exported: true namespace: "app_widgets" description: "Enable support for generated previews in AppWidgetManager" bug: "306546610" @@ -26,6 +27,7 @@ flag { flag { name: "draw_data_parcel" + is_exported: true namespace: "app_widgets" description: "Enable support for transporting draw instructions as data parcel" bug: "286130467" diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 5e00b7a798d8..2c26389071ce 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -1086,7 +1086,7 @@ public final class CompanionDeviceManager { } Objects.requireNonNull(deviceAddress, "address cannot be null"); try { - mService.registerDevicePresenceListenerService(deviceAddress, + mService.legacyStartObservingDevicePresence(deviceAddress, mContext.getOpPackageName(), mContext.getUserId()); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); @@ -1128,7 +1128,7 @@ public final class CompanionDeviceManager { } Objects.requireNonNull(deviceAddress, "address cannot be null"); try { - mService.unregisterDevicePresenceListenerService(deviceAddress, + mService.legacyStopObservingDevicePresence(deviceAddress, mContext.getPackageName(), mContext.getUserId()); } catch (RemoteException e) { ExceptionUtils.propagateIfInstanceOf(e.getCause(), DeviceNotAssociatedException.class); @@ -1328,7 +1328,7 @@ public final class CompanionDeviceManager { @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceAppeared(int associationId) { try { - mService.notifyDeviceAppeared(associationId); + mService.notifySelfManagedDeviceAppeared(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -1350,7 +1350,7 @@ public final class CompanionDeviceManager { @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED) public void notifyDeviceDisappeared(int associationId) { try { - mService.notifyDeviceDisappeared(associationId); + mService.notifySelfManagedDeviceDisappeared(associationId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index 57d59e5e5bf0..1b00f90e1fb3 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -59,12 +59,16 @@ interface ICompanionDeviceManager { int userId); @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void registerDevicePresenceListenerService(in String deviceAddress, in String callingPackage, - int userId); + void legacyStartObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId); @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void unregisterDevicePresenceListenerService(in String deviceAddress, in String callingPackage, - int userId); + void legacyStopObservingDevicePresence(in String deviceAddress, in String callingPackage, int userId); + + @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") + void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); + + @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") + void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); boolean canPairWithoutPrompt(in String packageName, in String deviceMacAddress, int userId); @@ -93,9 +97,11 @@ interface ICompanionDeviceManager { @EnforcePermission("USE_COMPANION_TRANSPORTS") void removeOnMessageReceivedListener(int messageType, IOnMessageReceivedListener listener); - void notifyDeviceAppeared(int associationId); + @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED") + void notifySelfManagedDeviceAppeared(int associationId); - void notifyDeviceDisappeared(int associationId); + @EnforcePermission("REQUEST_COMPANION_SELF_MANAGED") + void notifySelfManagedDeviceDisappeared(int associationId); PendingIntent buildPermissionTransferUserConsentIntent(String callingPackage, int userId, int associationId); @@ -135,10 +141,4 @@ interface ICompanionDeviceManager { byte[] getBackupPayload(int userId); void applyRestoredPayload(in byte[] payload, int userId); - - @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); - - @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") - void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); } diff --git a/core/java/android/companion/flags.aconfig b/core/java/android/companion/flags.aconfig index d634b64b1a4e..ecc5e1bd194f 100644 --- a/core/java/android/companion/flags.aconfig +++ b/core/java/android/companion/flags.aconfig @@ -2,6 +2,7 @@ package: "android.companion" flag { name: "new_association_builder" + is_exported: true namespace: "companion" description: "Controls if the new Builder is exposed to test apis." bug: "296251481" @@ -16,6 +17,7 @@ flag { flag { name: "association_tag" + is_exported: true namespace: "companion" description: "Enable Association tag APIs " bug: "289241123" @@ -23,6 +25,7 @@ flag { flag { name: "device_presence" + is_exported: true namespace: "companion" description: "Enable device presence APIs" bug: "283000075" @@ -30,6 +33,7 @@ flag { flag { name: "perm_sync_user_consent" + is_exported: true namespace: "companion" description: "Expose perm sync user consent API" bug: "309528663" diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index 6eab363c4eb1..30a1135d6be4 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -79,6 +79,11 @@ interface IVirtualDevice { int getDevicePolicy(int policyType); /** + * Returns whether the device has a valid microphone. + */ + boolean hasCustomAudioInputSupport(); + + /** * Closes the virtual device and frees all associated resources. */ @EnforcePermission("CREATE_VIRTUAL_DEVICE") diff --git a/core/java/android/companion/virtual/VirtualDevice.java b/core/java/android/companion/virtual/VirtualDevice.java index 97fa2ba2638d..b9e9afea8893 100644 --- a/core/java/android/companion/virtual/VirtualDevice.java +++ b/core/java/android/companion/virtual/VirtualDevice.java @@ -17,7 +17,6 @@ package android.companion.virtual; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; -import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; @@ -176,8 +175,7 @@ public final class VirtualDevice implements Parcelable { @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public boolean hasCustomAudioInputSupport() { try { - return mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO) == DEVICE_POLICY_CUSTOM; - // TODO(b/291735254): also check for a custom audio injection mix for this device id. + return mVirtualDevice.hasCustomAudioInputSupport(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 3304475df89f..ec59cf61097b 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -972,6 +972,7 @@ public final class VirtualDeviceManager { * * @param config camera configuration. * @return newly created camera. + * @throws UnsupportedOperationException if virtual camera isn't supported on this device. * @see VirtualDeviceParams#POLICY_TYPE_CAMERA */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig index 588e4fce1f3d..a6a4f5e77515 100644 --- a/core/java/android/companion/virtual/flags.aconfig +++ b/core/java/android/companion/virtual/flags.aconfig @@ -19,6 +19,7 @@ flag { flag { name: "dynamic_policy" + is_exported: true namespace: "virtual_devices" description: "Enable dynamic policy API" bug: "298401780" @@ -26,6 +27,7 @@ flag { flag { name: "cross_device_clipboard" + is_exported: true namespace: "virtual_devices" description: "Enable cross-device clipboard API" bug: "306622082" @@ -40,6 +42,7 @@ flag { flag { name: "vdm_custom_ime" + is_exported: true namespace: "virtual_devices" description: "Enable custom IME API" bug: "287269288" @@ -47,6 +50,7 @@ flag { flag { name: "vdm_custom_home" + is_exported: true namespace: "virtual_devices" description: "Enable custom home API" bug: "297168328" @@ -54,6 +58,7 @@ flag { flag { name: "vdm_public_apis" + is_exported: true namespace: "virtual_devices" description: "Enable public VDM API for device capabilities" bug: "297253526" @@ -61,6 +66,7 @@ flag { flag { name: "virtual_camera" + is_exported: true namespace: "virtual_devices" description: "Enable Virtual Camera" bug: "270352264" @@ -82,6 +88,7 @@ flag { flag { name: "persistent_device_id_api" + is_exported: true namespace: "virtual_devices" description: "Enable persistent device ID notification API" bug: "295258915" @@ -96,6 +103,7 @@ flag { flag { name: "interactive_screen_mirror" + is_exported: true namespace: "virtual_devices" description: "Enable interactive screen mirroring using Virtual Devices" bug: "292212199" @@ -103,6 +111,7 @@ flag { flag { name: "virtual_stylus" + is_exported: true namespace: "virtual_devices" description: "Enable virtual stylus input" bug: "304829446" diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 24d6a5cfc42d..2904e7c989e8 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -44,3 +44,11 @@ flag { description: "Enable device awareness in camera service" bug: "305170199" } + +flag { + namespace: "virtual_devices" + name: "device_aware_drm" + description: "Makes MediaDrm APIs device-aware" + bug: "303535376" + is_fixed_read_only: true +}
\ No newline at end of file diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 89300e3a15f1..c0c91cbdbc35 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4208,7 +4208,7 @@ public abstract class Context { MEDIA_COMMUNICATION_SERVICE, BATTERY_SERVICE, JOB_SCHEDULER_SERVICE, - //@hide: PERSISTENT_DATA_BLOCK_SERVICE, + PERSISTENT_DATA_BLOCK_SERVICE, //@hide: OEM_LOCK_SERVICE, MEDIA_PROJECTION_SERVICE, MIDI_SERVICE, @@ -5067,7 +5067,6 @@ public abstract class Context { * {@link android.hardware.fingerprint.FingerprintManager} for handling management * of fingerprints. * - * @removed See {@link android.hardware.biometrics.BiometricPrompt} * @see #getSystemService(String) * @see android.hardware.fingerprint.FingerprintManager */ @@ -5930,9 +5929,8 @@ public abstract class Context { * * @see #getSystemService(String) * @see android.service.persistentdata.PersistentDataBlockManager - * @hide */ - @SystemApi + @FlaggedApi(android.security.Flags.FLAG_FRP_ENFORCEMENT) public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; /** diff --git a/core/java/android/content/flags/flags.aconfig b/core/java/android/content/flags/flags.aconfig index 3445fb53d307..27bce5bb83dd 100644 --- a/core/java/android/content/flags/flags.aconfig +++ b/core/java/android/content/flags/flags.aconfig @@ -2,6 +2,7 @@ package: "android.content.flags" flag { name: "enable_bind_package_isolated_process" + is_exported: true namespace: "machine_learning" description: "This flag enables the newly added flag for binding package-private isolated processes." bug: "312706530" diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 39b914975362..6168b6800adc 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -29,6 +29,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; @@ -694,9 +695,16 @@ public class LauncherApps { * <p>If the caller is running on a managed profile, it'll return only the current profile. * Otherwise it'll return the same list as {@link UserManager#getUserProfiles()} would. * - * <p> To get hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>To get hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public List<UserHandle> getProfiles() { @@ -756,15 +764,21 @@ public class LauncherApps { * list.</li> * </ul> * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param packageName The specific package to query. If null, it checks all installed packages * in the profile. * @param user The UserHandle of the profile. * @return List of launchable activities. Can be an empty list but will not be null. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) { @@ -806,15 +820,21 @@ public class LauncherApps { * Returns information related to a user which is useful for displaying UI elements * to distinguish it from other users (eg, badges). * - * <p>If the user in question is a hidden profile like - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param userHandle user handle of the user for which LauncherUserInfo is requested. * @return the {@link LauncherUserInfo} object related to the user specified, null in case * the user is inaccessible. */ @Nullable + @SuppressLint("RequiresPermission") @FlaggedApi(Flags.FLAG_ALLOW_PRIVATE_PROFILE) @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) @@ -853,9 +873,14 @@ public class LauncherApps { * </ul> * </p> * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param packageName the package for which intent sender to launch App Market Activity is * required. @@ -864,6 +889,7 @@ public class LauncherApps { * there is no such activity. */ @Nullable + @SuppressLint("RequiresPermission") @FlaggedApi(Flags.FLAG_ALLOW_PRIVATE_PROFILE) @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) @@ -887,9 +913,14 @@ public class LauncherApps { * <p>An empty list denotes that all system packages should be treated as pre-installed for that * user at creation. * - * <p>If the user in question is a hidden profile like - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param userHandle the user for which installed system packages are required. * @return {@link List} of {@link String}, representing the package name of the installed @@ -897,6 +928,7 @@ public class LauncherApps { */ @FlaggedApi(Flags.FLAG_ALLOW_PRIVATE_PROFILE) @NonNull + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public List<String> getPreInstalledSystemPackages(@NonNull UserHandle userHandle) { @@ -936,14 +968,20 @@ public class LauncherApps { * Returns the activity info for a given intent and user handle, if it resolves. Otherwise it * returns null. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param intent The intent to find a match for. * @param user The profile to look in for a match. * @return An activity info object if there is a match. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public LauncherActivityInfo resolveActivity(Intent intent, UserHandle user) { @@ -995,15 +1033,21 @@ public class LauncherApps { /** * Starts a Main activity in the specified profile. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param component The ComponentName of the activity to launch * @param user The UserHandle of the profile * @param sourceBounds The Rect containing the source bounds of the clicked icon * @param opts Options to pass to startActivity */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public void startMainActivity(ComponentName component, UserHandle user, Rect sourceBounds, @@ -1043,15 +1087,21 @@ public class LauncherApps { * Starts the settings activity to show the application details for a * package in the specified profile. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param component The ComponentName of the package to launch settings for. * @param user The UserHandle of the profile * @param sourceBounds The Rect containing the source bounds of the clicked icon * @param opts Options to pass to startActivity */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public void startAppDetailsActivity(ComponentName component, UserHandle user, @@ -1097,7 +1147,8 @@ public class LauncherApps { * @param packageName The specific package to query. If null, it checks all installed packages * in the profile. * @param user The UserHandle of the profile. - * @return List of config activities. Can be an empty list but will not be null. + * @return List of config activities. Can be an empty list but will not be null. Empty list will + * be returned for user-profiles that have items restricted on home screen. * * @see Intent#ACTION_CREATE_SHORTCUT * @see #getShortcutConfigActivityIntent(LauncherActivityInfo) @@ -1164,15 +1215,21 @@ public class LauncherApps { /** * Checks if the package is installed and enabled for a profile. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param packageName The package to check. * @param user The UserHandle of the profile. * * @return true if the package exists and is enabled. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public boolean isPackageEnabled(String packageName, UserHandle user) { @@ -1192,9 +1249,14 @@ public class LauncherApps { * <p>The contents of this {@link Bundle} are supposed to be a contract between the suspending * app and the launcher. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * <p>Note: This just returns whatever extras were provided to the system, <em>which might * even be {@code null}.</em> @@ -1207,6 +1269,7 @@ public class LauncherApps { * @see Callback#onPackagesSuspended(String[], UserHandle, Bundle) * @see PackageManager#isPackageSuspended() */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public @Nullable Bundle getSuspendedPackageLauncherExtras(String packageName, UserHandle user) { @@ -1223,14 +1286,20 @@ public class LauncherApps { * could be done because the package was marked as distracting to the user via * {@code PackageManager.setDistractingPackageRestrictions(String[], int)}. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param packageName The package for which to check. * @param user the {@link UserHandle} of the profile. * @return */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public boolean shouldHideFromSuggestions(@NonNull String packageName, @@ -1247,9 +1316,14 @@ public class LauncherApps { /** * Returns {@link ApplicationInfo} about an application installed for a specific user profile. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param packageName The package name of the application * @param flags Additional option flags {@link PackageManager#getApplicationInfo} @@ -1259,6 +1333,7 @@ public class LauncherApps { * {@code null} if the package isn't installed for the given profile, or the profile * isn't enabled. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public ApplicationInfo getApplicationInfo(@NonNull String packageName, @@ -1310,15 +1385,21 @@ public class LauncherApps { * <p>The activity may still not be exported, in which case {@link #startMainActivity} will * throw a {@link SecurityException} unless the caller has the same UID as the target app's. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>If the user in question is a hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param component The activity to check. * @param user The UserHandle of the profile. * * @return true if the activity exists and is enabled. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public boolean isActivityEnabled(ComponentName component, UserHandle user) { @@ -1488,6 +1569,9 @@ public class LauncherApps { * <p>The calling launcher application must be allowed to access the shortcut information, * as defined in {@link #hasShortcutHostPermission()}. * + * <p>For user-profiles with items restricted on home screen, caller must have the required + * permission. + * * @param packageName The target package name. * @param shortcutIds The IDs of the shortcut to be pinned. * @param user The UserHandle of the profile. @@ -1496,6 +1580,7 @@ public class LauncherApps { * * @see ShortcutManager */ + @RequiresPermission(conditional = true, value = android.Manifest.permission.ACCESS_SHORTCUTS) public void pinShortcuts(@NonNull String packageName, @NonNull List<String> shortcutIds, @NonNull UserHandle user) { logErrorForInvalidProfileAccess(user); @@ -1875,12 +1960,18 @@ public class LauncherApps { /** * Registers a callback for changes to packages in this user and managed profiles. * - * <p>To receive callbacks for hidden profile{@link UserManager.USER_TYPE_PROFILE_PRIVATE}, - * caller should have {@link android.app.role.RoleManager.ROLE_HOME} and either of the - * permissions required. + * <p>To receive callbacks for hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param callback The callback to register. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public void registerCallback(Callback callback) { @@ -1891,12 +1982,18 @@ public class LauncherApps { * Registers a callback for changes to packages in this user and managed profiles. * * <p>To receive callbacks for hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, - * caller should have {@link android.app.role.RoleManager.ROLE_HOME} and either of the - * permissions required. + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @param callback The callback to register. * @param handler that should be used to post callbacks on, may be null. */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public void registerCallback(Callback callback, Handler handler) { @@ -2298,6 +2395,9 @@ public class LauncherApps { * app's manifest, have the android.permission.QUERY_ALL_PACKAGES, or be the session owner to * watch for these events. * + * <p> Session callbacks are not sent for user-profiles that have items restricted on home + * screen. + * * @param callback The callback to register. * @param executor {@link Executor} to handle the callbacks, cannot be null. * @@ -2346,12 +2446,18 @@ public class LauncherApps { * package name in the app's manifest, have the android.permission.QUERY_ALL_PACKAGES, or be * the session owner to retrieve these details. * - * <p>If the user in question is a hidden profile - * {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, caller should have - * {@link android.app.role.RoleManager.ROLE_HOME} and either of the permissions required. + * <p>To receive callbacks for hidden profile {@link UserManager.USER_TYPE_PROFILE_PRIVATE}, + * caller should have either:</p> + * <ul> + * <li>the privileged {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} + * permission</li> + * <li>the normal {@link android.Manifest.permission.ACCESS_HIDDEN_PROFILES} permission and the + * {@link android.app.role.RoleManager.ROLE_HOME} role. </li> + * </ul> * * @see PackageInstaller#getAllSessions() */ + @SuppressLint("RequiresPermission") @RequiresPermission(conditional = true, anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES}) public @NonNull List<SessionInfo> getAllPackageInstallerSessions() { diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 17e6f167b06d..270fc32a4e32 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -177,6 +177,10 @@ public class PackageInstaller { * Broadcast Action: Explicit broadcast sent to the last known default launcher when a session * for a new install is committed. For managed profile, this is sent to the default launcher * of the primary profile. + * For user-profiles that have items restricted on home screen, this broadcast is sent to + * the default launcher of the primary profile, only if it has either + * {@link Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL} or + * {@link Manifest.permission.ACCESS_HIDDEN_PROFILES} permission. * <p> * The associated session is defined in {@link #EXTRA_SESSION} and the user for which this * session was created in {@link Intent#EXTRA_USER}. diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 9f2f74b66eb3..41f093614e6c 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -895,7 +895,7 @@ public abstract class PackageManager { GET_DISABLED_COMPONENTS, GET_DISABLED_UNTIL_USED_COMPONENTS, GET_UNINSTALLED_PACKAGES, - MATCH_CLONE_PROFILE, + MATCH_CLONE_PROFILE_LONG, MATCH_QUARANTINED_COMPONENTS, }) @Retention(RetentionPolicy.SOURCE) @@ -1235,10 +1235,11 @@ public abstract class PackageManager { public static final int MATCH_DEBUG_TRIAGED_MISSING = MATCH_DIRECT_BOOT_AUTO; /** - * Use {@link #MATCH_CLONE_PROFILE_LONG} instead. + * @deprecated Use {@link #MATCH_CLONE_PROFILE_LONG} instead. * * @hide */ + @Deprecated @SystemApi public static final int MATCH_CLONE_PROFILE = 0x20000000; @@ -3262,6 +3263,16 @@ public abstract class PackageManager { /** * Feature for {@link #getSystemAvailableFeatures} and + * {@link #hasSystemFeature}: This device is capable of launching apps in automotive display + * compatibility mode. + * @hide + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_CAR_DISPLAY_COMPATIBILITY = + "android.software.car.display_compatibility"; + + /** + * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature(String, int)}: If this feature is supported, the device supports * {@link android.security.identity.IdentityCredentialStore} implemented in secure hardware * at the given feature version. diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java index e48a02a192d2..3514914b1d86 100644 --- a/core/java/android/content/pm/ShortcutManager.java +++ b/core/java/android/content/pm/ShortcutManager.java @@ -583,8 +583,8 @@ public class ShortcutManager { * * @return {@code TRUE} if the launcher supports this feature. Note the API will return without * waiting for the user to respond, so getting {@code TRUE} from this API does *not* mean - * the shortcut was pinned successfully. {@code FALSE} if the launcher doesn't support this - * feature. + * the shortcut was pinned successfully. {@code FALSE} if the launcher doesn't support this + * feature or if calling app belongs to a user-profile with items restricted on home screen. * * @see #isRequestPinShortcutSupported() * @see IntentSender diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 92cb9cc1d8dc..cde565b3f66e 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -2,6 +2,7 @@ package: "android.content.pm" flag { name: "quarantined_enabled" + is_exported: true namespace: "package_manager_service" description: "Feature flag for Quarantined state" bug: "269127435" @@ -9,6 +10,7 @@ flag { flag { name: "archiving" + is_exported: true namespace: "package_manager_service" description: "Feature flag to enable the archiving feature." bug: "278553670" @@ -24,6 +26,7 @@ flag { flag { name: "stay_stopped" + is_exported: true namespace: "backstage_power" description: "Feature flag to improve stopped state enforcement" bug: "296644915" @@ -39,6 +42,7 @@ flag { flag { name: "get_package_info" + is_exported: true namespace: "package_manager_service" description: "Feature flag to enable the feature to retrieve package info without installation." bug: "269149275" @@ -54,6 +58,7 @@ flag { flag { name: "sdk_lib_independence" + is_exported: true namespace: "package_manager_service" description: "Feature flag to keep app working even if its declared sdk-library dependency is unavailable." bug: "295827951" @@ -78,6 +83,7 @@ flag { flag { name: "get_resolved_apk_path" + is_exported: true namespace: "package_manager_service" description: "Feature flag to retrieve resolved path of the base APK during an app install." bug: "269728874" @@ -92,6 +98,7 @@ flag { flag { name: "read_install_info" + is_exported: true namespace: "package_manager_service" description: "Feature flag to read install related information from an APK." bug: "275658500" @@ -113,6 +120,7 @@ flag { flag { name: "relative_reference_intent_filters" + is_exported: true namespace: "package_manager_service" description: "Feature flag to enable relative reference intent filters" bug: "307556883" @@ -121,6 +129,7 @@ flag { flag { name: "fix_duplicated_flags" + is_exported: true namespace: "package_manager_service" description: "Feature flag to fix duplicated PackageManager flag values" bug: "314815969" @@ -128,6 +137,7 @@ flag { flag { name: "provide_info_of_apk_in_apex" + is_exported: true namespace: "package_manager_service" description: "Feature flag to provide the information of APK-in-APEX" bug: "306329516" @@ -144,6 +154,7 @@ flag { flag { name: "introduce_media_processing_type" + is_exported: true namespace: "backstage_power" description: "Add a new FGS type for media processing use cases." bug: "317788011" @@ -182,6 +193,7 @@ flag { flag { name: "emergency_install_permission" + is_exported: true namespace: "permissions" description: "Feature flag to enable permission EMERGENCY_INSTALL_PACKAGES" bug: "321080601" @@ -189,6 +201,7 @@ flag { flag { name: "asl_in_apk_app_metadata_source" + is_exported: true namespace: "package_manager_service" description: "Feature flag to allow to know if the Android Safety Label (ASL) of an app is provided by the app's APK itself, or provided by an installer." bug: "287487923" @@ -205,6 +218,7 @@ flag { flag { name: "set_pre_verified_domains" + is_exported: true namespace: "package_manager_service" description: "Feature flag to enable pre-verified domains" bug: "307327678" diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 2d32aed5a1ad..4963a4f27803 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -24,6 +24,7 @@ flag { flag { name: "support_communal_profile" + is_exported: true namespace: "multiuser" description: "Framework support for communal profile." bug: "285426179" @@ -31,6 +32,7 @@ flag { flag { name: "support_communal_profile_nextgen" + is_exported: true namespace: "multiuser" description: "Further framework support for communal profile, beyond the basics, for later releases." bug: "285426179" @@ -59,6 +61,7 @@ flag { flag { name: "enable_biometrics_to_unlock_private_space" + is_exported: true namespace: "profile_experiences" description: "Add support to unlock the private space using biometrics" bug: "312184187" @@ -102,6 +105,7 @@ flag { flag { name: "enable_system_user_only_for_services_and_providers" + is_exported: true namespace: "multiuser" description: "Enable systemUserOnly manifest attribute for services and providers." bug: "302354856" @@ -118,6 +122,7 @@ flag { flag { name: "enable_permission_to_access_hidden_profiles" + is_exported: true namespace: "profile_experiences" description: "Add permission to access API hidden users data via system APIs" bug: "321988638" diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index d27479299efa..885f4c5e8ec4 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -802,14 +802,20 @@ public final class Configuration implements Parcelable, Comparable<Configuration public static final int SCREEN_WIDTH_DP_UNDEFINED = 0; /** - * The width of the available screen space in dp units excluding the area - * occupied by {@link android.view.WindowInsets window insets}. + * The width of the available screen space in dp units. * - * <aside class="note"><b>Note:</b> The width measurement excludes window - * insets even when the app is displayed edge to edge using - * {@link android.view.Window#setDecorFitsSystemWindows(boolean) + * <aside class="note"><b>Note:</b> If the app targets + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} + * or after, The width measurement reflects the window size without excluding insets. + * Otherwise, the measurement excludes window insets even when the app is displayed edge to edge + * using {@link android.view.Window#setDecorFitsSystemWindows(boolean) * Window#setDecorFitsSystemWindows(boolean)}.</aside> * + * Use {@link android.view.WindowMetrics#getBounds()} to always obtain the horizontal + * display area available to an app or embedded activity including the area + * occupied by window insets. A version of the API is also available for use on older platforms + * through {@link androidx.window.layout.WindowMetrics}. + * * <p>Corresponds to the * <a href="{@docRoot}guide/topics/resources/providing-resources.html#AvailableWidthHeightQualifier"> * available width</a> resource qualifier. Defaults to @@ -831,14 +837,15 @@ public final class Configuration implements Parcelable, Comparable<Configuration * environment, {@code screenWidthDp} is the width of the screen on which * the app is displayed excluding window insets. * - * <p>Differs from {@link android.view.WindowMetrics} by not including + * <p>If the app targets {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} or after, + * it is the same as {@link android.view.WindowMetrics}, but is expressed rounded to the nearest + * dp rather than px. + * + * <p>Otherwise, differs from {@link android.view.WindowMetrics} by not including * window insets in the width measurement and by expressing the measurement * in dp rather than px. Use {@code screenWidthDp} to obtain the width of * the display area available to an app or embedded activity excluding the - * area occupied by window insets. Use - * {@link android.view.WindowMetrics#getBounds()} to obtain the horizontal - * display area available to an app or embedded activity including the area - * occupied by window insets. + * area occupied by window insets. */ public int screenWidthDp; @@ -849,15 +856,20 @@ public final class Configuration implements Parcelable, Comparable<Configuration public static final int SCREEN_HEIGHT_DP_UNDEFINED = 0; /** - * The height of the available screen space in dp units excluding the area - * occupied by {@link android.view.WindowInsets window insets}, such as the - * status bar, navigation bar, and cutouts. + * The height of the available screen space in dp units. * - * <aside class="note"><b>Note:</b> The height measurement excludes window - * insets even when the app is displayed edge to edge using - * {@link android.view.Window#setDecorFitsSystemWindows(boolean) + * <aside class="note"><b>Note:</b> If the app targets + * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} + * or after, the height measurement reflects the window size without excluding insets. + * Otherwise, the measurement excludes window insets even when the app is displayed edge to edge + * using {@link android.view.Window#setDecorFitsSystemWindows(boolean) * Window#setDecorFitsSystemWindows(boolean)}.</aside> * + * Use {@link android.view.WindowMetrics#getBounds()} to always obtain the vertical + * display area available to an app or embedded activity including the area + * occupied by window insets. A version of the API is also available for use on older platforms + * through {@link androidx.window.layout.WindowMetrics}. + * * <p>Corresponds to the * <a href="{@docRoot}guide/topics/resources/providing-resources.html#AvailableWidthHeightQualifier"> * available height</a> resource qualifier. Defaults to @@ -879,14 +891,15 @@ public final class Configuration implements Parcelable, Comparable<Configuration * multiple-screen environment, {@code screenHeightDp} is the height of the * screen on which the app is displayed excluding window insets. * - * <p>Differs from {@link android.view.WindowMetrics} by not including + * <p>If the app targets {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} or after, + * it is the same as {@link android.view.WindowMetrics}, but is expressed rounded to the nearest + * dp rather than px. + * + * <p>Otherwise, differs from {@link android.view.WindowMetrics} by not including * window insets in the height measurement and by expressing the measurement * in dp rather than px. Use {@code screenHeightDp} to obtain the height of * the display area available to an app or embedded activity excluding the - * area occupied by window insets. Use - * {@link android.view.WindowMetrics#getBounds()} to obtain the vertical - * display area available to an app or embedded activity including the area - * occupied by window insets. + * area occupied by window insets. */ public int screenHeightDp; diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig index 7fd0b03b213d..8f5c912d8c03 100644 --- a/core/java/android/content/res/flags.aconfig +++ b/core/java/android/content/res/flags.aconfig @@ -2,6 +2,7 @@ package: "android.content.res" flag { name: "default_locale" + is_exported: true namespace: "resource_manager" description: "Feature flag for default locale in LocaleConfig" bug: "117306409" @@ -11,6 +12,7 @@ flag { flag { name: "font_scale_converter_public" + is_exported: true namespace: "accessibility" description: "Enables the public API for FontScaleConverter, including enabling thread-safe caching." bug: "239736383" @@ -20,6 +22,7 @@ flag { flag { name: "asset_file_descriptor_frro" + is_exported: true namespace: "resource_manager" description: "Feature flag for passing in an AssetFileDescriptor to create an frro" bug: "304478666" @@ -27,6 +30,7 @@ flag { flag { name: "manifest_flagging" + is_exported: true namespace: "resource_manager" description: "Feature flag for flagging manifest entries" bug: "297373084" @@ -36,6 +40,7 @@ flag { flag { name: "nine_patch_frro" + is_exported: true namespace: "resource_manager" description: "Feature flag for creating an frro from a 9-patch" bug: "296324826" @@ -43,6 +48,7 @@ flag { flag { name: "register_resource_paths" + is_exported: true namespace: "resource_manager" description: "Feature flag for register resource paths for shared library" bug: "306202569" diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig index 47edba6a9e56..d0773297a4a0 100644 --- a/core/java/android/credentials/flags.aconfig +++ b/core/java/android/credentials/flags.aconfig @@ -3,6 +3,7 @@ package: "android.credentials.flags" flag { namespace: "credential_manager" name: "settings_activity_enabled" + is_exported: true description: "Enable the Credential Manager Settings Activity APIs" bug: "300014059" } @@ -24,6 +25,7 @@ flag { flag { namespace: "credential_manager" name: "new_settings_intents" + is_exported: true description: "Enables settings intents to redirect to new settings page" bug: "307587989" } @@ -45,8 +47,10 @@ flag { flag { namespace: "credential_manager" name: "configurable_selector_ui_enabled" + is_exported: true description: "Enables OEM configurable Credential Selector UI" bug: "319448437" + is_exported: true } flag { diff --git a/core/java/android/credentials/selection/IntentCreationResult.java b/core/java/android/credentials/selection/IntentCreationResult.java new file mode 100644 index 000000000000..189ff7bbcb6e --- /dev/null +++ b/core/java/android/credentials/selection/IntentCreationResult.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials.selection; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Intent; + +/** + * Result of creating a Credential Manager UI intent. + * + * @hide + */ +public final class IntentCreationResult { + @NonNull + private final Intent mIntent; + @Nullable + private final String mFallbackUiPackageName; + @Nullable + private final String mOemUiPackageName; + @NonNull + private final OemUiUsageStatus mOemUiUsageStatus; + + private IntentCreationResult(@NonNull Intent intent, @Nullable String fallbackUiPackageName, + @Nullable String oemUiPackageName, OemUiUsageStatus oemUiUsageStatus) { + mIntent = intent; + mFallbackUiPackageName = fallbackUiPackageName; + mOemUiPackageName = oemUiPackageName; + mOemUiUsageStatus = oemUiUsageStatus; + } + + /** Returns the UI intent. */ + @NonNull + public Intent getIntent() { + return mIntent; + } + + /** + * Returns the result of attempting to use the config_oemCredentialManagerDialogComponent + * as the Credential Manager UI. + */ + @NonNull + public OemUiUsageStatus getOemUiUsageStatus() { + return mOemUiUsageStatus; + } + + /** + * Returns the package name of the ui component specified in + * config_fallbackCredentialManagerDialogComponent, or null if unspecified / not parsable + * successfully. + */ + @Nullable + public String getFallbackUiPackageName() { + return mFallbackUiPackageName; + } + + /** + * Returns the package name of the oem ui component specified in + * config_oemCredentialManagerDialogComponent, or null if unspecified / not parsable. + */ + @Nullable + public String getOemUiPackageName() { + return mOemUiPackageName; + } + + /** + * Result of attempting to use the config_oemCredentialManagerDialogComponent as the Credential + * Manager UI. + */ + public enum OemUiUsageStatus { + UNKNOWN, + // Success: the UI specified in config_oemCredentialManagerDialogComponent was used to + // fulfill the request. + SUCCESS, + // The config value was not specified (e.g. left empty). + OEM_UI_CONFIG_NOT_SPECIFIED, + // The config value component was specified but not found (e.g. component doesn't exist or + // component isn't a system app). + OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND, + // The config value component was found but not enabled. + OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED, + } + + /** + * Builder for {@link IntentCreationResult}. + * + * @hide + */ + public static final class Builder { + @NonNull + private Intent mIntent; + @Nullable + private String mFallbackUiPackageName = null; + @Nullable + private String mOemUiPackageName = null; + @NonNull + private OemUiUsageStatus mOemUiUsageStatus = OemUiUsageStatus.UNKNOWN; + + public Builder(Intent intent) { + mIntent = intent; + } + + /** + * Sets the package name of the ui component specified in + * config_fallbackCredentialManagerDialogComponent, or null if unspecified / not parsable + * successfully. + */ + @NonNull + public Builder setFallbackUiPackageName(@Nullable String fallbackUiPackageName) { + mFallbackUiPackageName = fallbackUiPackageName; + return this; + } + + /** + * Sets the package name of the oem ui component specified in + * config_oemCredentialManagerDialogComponent, or null if unspecified / not parsable. + */ + @NonNull + public Builder setOemUiPackageName(@Nullable String oemUiPackageName) { + mOemUiPackageName = oemUiPackageName; + return this; + } + + /** + * Sets the result of attempting to use the config_oemCredentialManagerDialogComponent + * as the Credential Manager UI. + */ + @NonNull + public Builder setOemUiUsageStatus(OemUiUsageStatus oemUiUsageStatus) { + mOemUiUsageStatus = oemUiUsageStatus; + return this; + } + + /** Builds a {@link IntentCreationResult}. */ + @NonNull + public IntentCreationResult build() { + return new IntentCreationResult(mIntent, mFallbackUiPackageName, mOemUiPackageName, + mOemUiUsageStatus); + } + } +} diff --git a/core/java/android/credentials/selection/IntentFactory.java b/core/java/android/credentials/selection/IntentFactory.java index 79fba9b19250..b98a0d825227 100644 --- a/core/java/android/credentials/selection/IntentFactory.java +++ b/core/java/android/credentials/selection/IntentFactory.java @@ -36,6 +36,8 @@ import android.os.ResultReceiver; import android.text.TextUtils; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; + import java.util.ArrayList; /** @@ -57,22 +59,104 @@ public class IntentFactory { * @hide */ @NonNull - public static Intent createCredentialSelectorIntentForAutofill( + public static IntentCreationResult createCredentialSelectorIntentForAutofill( + @NonNull Context context, + @NonNull RequestInfo requestInfo, + @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. + @NonNull + ArrayList<DisabledProviderData> disabledProviderDataList, + @NonNull ResultReceiver resultReceiver) { + return createCredentialSelectorIntentInternal(context, requestInfo, + disabledProviderDataList, resultReceiver); + } + + /** + * Generate a new launch intent to the Credential Selector UI. + * + * @param context the CredentialManager system service (only expected caller) + * context that may be used to query existence of the key UI + * application + * @param disabledProviderDataList the list of disabled provider data that when non-empty the + * UI should accordingly generate an entry suggesting the user + * to navigate to settings and enable them + * @param enabledProviderDataList the list of enabled provider that contain options for this + * request; the UI should render each option to the user for + * selection + * @param requestInfo the display information about the given app request + * @param resultReceiver used by the UI to send the UI selection result back + * @hide + */ + @NonNull + public static IntentCreationResult createCredentialSelectorIntentForCredMan( @NonNull Context context, @NonNull RequestInfo requestInfo, @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. @NonNull + ArrayList<ProviderData> enabledProviderDataList, + @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. + @NonNull ArrayList<DisabledProviderData> disabledProviderDataList, @NonNull ResultReceiver resultReceiver) { - return createCredentialSelectorIntent(context, requestInfo, + IntentCreationResult result = createCredentialSelectorIntentInternal(context, requestInfo, disabledProviderDataList, resultReceiver); + result.getIntent().putParcelableArrayListExtra( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList); + return result; + } + + /** + * Generate a new launch intent to the Credential Selector UI. + * + * @param context the CredentialManager system service (only expected caller) + * context that may be used to query existence of the key UI + * application + * @param disabledProviderDataList the list of disabled provider data that when non-empty the + * UI should accordingly generate an entry suggesting the user + * to navigate to settings and enable them + * @param enabledProviderDataList the list of enabled provider that contain options for this + * request; the UI should render each option to the user for + * selection + * @param requestInfo the display information about the given app request + * @param resultReceiver used by the UI to send the UI selection result back + */ + @VisibleForTesting + @NonNull + public static Intent createCredentialSelectorIntent( + @NonNull Context context, + @NonNull RequestInfo requestInfo, + @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. + @NonNull + ArrayList<ProviderData> enabledProviderDataList, + @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. + @NonNull + ArrayList<DisabledProviderData> disabledProviderDataList, + @NonNull ResultReceiver resultReceiver) { + return createCredentialSelectorIntentForCredMan(context, requestInfo, + enabledProviderDataList, disabledProviderDataList, resultReceiver).getIntent(); + } + + /** + * Creates an Intent that cancels any UI matching the given request token id. + */ + @VisibleForTesting + @NonNull + public static Intent createCancelUiIntent(@NonNull Context context, + @NonNull IBinder requestToken, boolean shouldShowCancellationUi, + @NonNull String appPackageName) { + Intent intent = new Intent(); + IntentCreationResult.Builder intentResultBuilder = new IntentCreationResult.Builder(intent); + setCredentialSelectorUiComponentName(context, intent, intentResultBuilder); + intent.putExtra(CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST, + new CancelSelectionRequest(new RequestToken(requestToken), shouldShowCancellationUi, + appPackageName)); + return intent; } /** * Generate a new launch intent to the Credential Selector UI. */ @NonNull - private static Intent createCredentialSelectorIntent( + private static IntentCreationResult createCredentialSelectorIntentInternal( @NonNull Context context, @NonNull RequestInfo requestInfo, @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. @@ -80,25 +164,37 @@ public class IntentFactory { ArrayList<DisabledProviderData> disabledProviderDataList, @NonNull ResultReceiver resultReceiver) { Intent intent = new Intent(); - setCredentialSelectorUiComponentName(context, intent); + IntentCreationResult.Builder intentResultBuilder = new IntentCreationResult.Builder(intent); + setCredentialSelectorUiComponentName(context, intent, intentResultBuilder); intent.putParcelableArrayListExtra( ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList); intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo); intent.putExtra( Constants.EXTRA_RESULT_RECEIVER, toIpcFriendlyResultReceiver(resultReceiver)); - - return intent; + return intentResultBuilder.build(); } private static void setCredentialSelectorUiComponentName(@NonNull Context context, - @NonNull Intent intent) { + @NonNull Intent intent, @NonNull IntentCreationResult.Builder intentResultBuilder) { if (configurableSelectorUiEnabled()) { - ComponentName componentName = getOemOverrideComponentName(context); + ComponentName componentName = getOemOverrideComponentName(context, intentResultBuilder); + + ComponentName fallbackUiComponentName = null; + try { + fallbackUiComponentName = ComponentName.unflattenFromString( + Resources.getSystem().getString( + com.android.internal.R.string + .config_fallbackCredentialManagerDialogComponent)); + intentResultBuilder.setFallbackUiPackageName( + fallbackUiComponentName.getPackageName()); + } catch (Exception e) { + Slog.w(TAG, "Fallback CredMan IU not found: " + e); + } + if (componentName == null) { - componentName = ComponentName.unflattenFromString(Resources.getSystem().getString( - com.android.internal.R.string - .config_fallbackCredentialManagerDialogComponent)); + componentName = fallbackUiComponentName; } + intent.setComponent(componentName); } else { ComponentName componentName = ComponentName.unflattenFromString(Resources.getSystem() @@ -113,7 +209,8 @@ public class IntentFactory { * default platform UI component name should be used instead. */ @Nullable - private static ComponentName getOemOverrideComponentName(@NonNull Context context) { + private static ComponentName getOemOverrideComponentName(@NonNull Context context, + @NonNull IntentCreationResult.Builder intentResultBuilder) { ComponentName result = null; String oemComponentString = Resources.getSystem() @@ -121,86 +218,54 @@ public class IntentFactory { com.android.internal.R.string .config_oemCredentialManagerDialogComponent); if (!TextUtils.isEmpty(oemComponentString)) { - ComponentName oemComponentName = ComponentName.unflattenFromString( - oemComponentString); + ComponentName oemComponentName = null; + try { + oemComponentName = ComponentName.unflattenFromString( + oemComponentString); + } catch (Exception e) { + Slog.i(TAG, "Failed to parse OEM component name " + oemComponentString + ": " + e); + } if (oemComponentName != null) { try { + intentResultBuilder.setOemUiPackageName(oemComponentName.getPackageName()); ActivityInfo info = context.getPackageManager().getActivityInfo( oemComponentName, PackageManager.ComponentInfoFlags.of( PackageManager.MATCH_SYSTEM_ONLY)); if (info.enabled && info.exported) { + intentResultBuilder.setOemUiUsageStatus(IntentCreationResult + .OemUiUsageStatus.SUCCESS); Slog.i(TAG, "Found enabled oem CredMan UI component." + oemComponentString); result = oemComponentName; } else { + intentResultBuilder.setOemUiUsageStatus(IntentCreationResult + .OemUiUsageStatus.OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED); Slog.i(TAG, "Found enabled oem CredMan UI component but it was not " + "enabled."); } } catch (PackageManager.NameNotFoundException e) { + intentResultBuilder.setOemUiUsageStatus(IntentCreationResult.OemUiUsageStatus + .OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND); Slog.i(TAG, "Unable to find oem CredMan UI component: " + oemComponentString + "."); } } else { + intentResultBuilder.setOemUiUsageStatus(IntentCreationResult.OemUiUsageStatus + .OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND); Slog.i(TAG, "Invalid OEM ComponentName format."); } } else { + intentResultBuilder.setOemUiUsageStatus( + IntentCreationResult.OemUiUsageStatus.OEM_UI_CONFIG_NOT_SPECIFIED); Slog.i(TAG, "Invalid empty OEM component name."); } return result; } /** - * Generate a new launch intent to the Credential Selector UI. - * - * @param context the CredentialManager system service (only expected caller) - * context that may be used to query existence of the key UI - * application - * @param disabledProviderDataList the list of disabled provider data that when non-empty the - * UI should accordingly generate an entry suggesting the user - * to navigate to settings and enable them - * @param enabledProviderDataList the list of enabled provider that contain options for this - * request; the UI should render each option to the user for - * selection - * @param requestInfo the display information about the given app request - * @param resultReceiver used by the UI to send the UI selection result back - */ - @NonNull - public static Intent createCredentialSelectorIntent( - @NonNull Context context, - @NonNull RequestInfo requestInfo, - @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. - @NonNull - ArrayList<ProviderData> enabledProviderDataList, - @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. - @NonNull - ArrayList<DisabledProviderData> disabledProviderDataList, - @NonNull ResultReceiver resultReceiver) { - Intent intent = createCredentialSelectorIntent(context, requestInfo, - disabledProviderDataList, resultReceiver); - intent.putParcelableArrayListExtra( - ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList); - return intent; - } - - /** - * Creates an Intent that cancels any UI matching the given request token id. - */ - @NonNull - public static Intent createCancelUiIntent(@NonNull Context context, - @NonNull IBinder requestToken, boolean shouldShowCancellationUi, - @NonNull String appPackageName) { - Intent intent = new Intent(); - setCredentialSelectorUiComponentName(context, intent); - intent.putExtra(CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST, - new CancelSelectionRequest(new RequestToken(requestToken), shouldShowCancellationUi, - appPackageName)); - return intent; - } - - /** * Convert an instance of a "locally-defined" ResultReceiver to an instance of {@link * android.os.ResultReceiver} itself, which the receiving process will be able to unmarshall. */ diff --git a/core/java/android/database/sqlite/flags.aconfig b/core/java/android/database/sqlite/flags.aconfig index 92ef9c24c4ef..7ecffaf01549 100644 --- a/core/java/android/database/sqlite/flags.aconfig +++ b/core/java/android/database/sqlite/flags.aconfig @@ -2,6 +2,7 @@ package: "android.database.sqlite" flag { name: "sqlite_apis_35" + is_exported: true namespace: "system_performance" is_fixed_read_only: true description: "SQLite APIs held back for Android 15" diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index a8d54ed970b7..7d61c142fa04 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -123,6 +123,15 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan public static final int DISMISSED_REASON_CREDENTIAL_CONFIRMED = 7; /** + * Dialog is done animating away after user clicked on the button set via + * {@link PromptContentViewWithMoreOptionsButton.Builder#setMoreOptionsButtonListener(Executor, + * DialogInterface.OnClickListener)} )}. + * + * @hide + */ + public static final int DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS = 8; + + /** * @hide */ @IntDef({DISMISSED_REASON_BIOMETRIC_CONFIRMED, @@ -131,7 +140,8 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED, DISMISSED_REASON_ERROR, DISMISSED_REASON_SERVER_REQUESTED, - DISMISSED_REASON_CREDENTIAL_CONFIRMED}) + DISMISSED_REASON_CREDENTIAL_CONFIRMED, + DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS}) @Retention(RetentionPolicy.SOURCE) public @interface DismissedReason {} @@ -654,8 +664,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan private final IAuthService mService; private final PromptInfo mPromptInfo; private final ButtonInfo mNegativeButtonInfo; - // TODO(b/328843028): add callback onContentViewMoreOptionsButtonClicked() in - // IBiometricServiceReceiver. private final ButtonInfo mContentViewMoreOptionsButtonInfo; private CryptoObject mCryptoObject; @@ -745,6 +753,13 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan mNegativeButtonInfo.listener.onClick(null, DialogInterface.BUTTON_NEGATIVE); mIsPromptShowing = false; }); + } else if (reason == DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS) { + if (mContentViewMoreOptionsButtonInfo != null) { + mContentViewMoreOptionsButtonInfo.executor.execute(() -> { + mContentViewMoreOptionsButtonInfo.listener.onClick(null, + DialogInterface.BUTTON_NEGATIVE); + }); + } } else { mIsPromptShowing = false; Log.e(TAG, "Unknown reason: " + reason); diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig index ff07498836af..9836eece19fe 100644 --- a/core/java/android/hardware/biometrics/flags.aconfig +++ b/core/java/android/hardware/biometrics/flags.aconfig @@ -18,6 +18,7 @@ flag { flag { name: "get_op_id_crypto_object" + is_exported: true namespace: "biometrics_framework" description: "Feature flag for adding a get operation id api to CryptoObject." bug: "307601768" @@ -25,8 +26,8 @@ flag { flag { name: "custom_biometric_prompt" + is_exported: true namespace: "biometrics_framework" description: "Feature flag for adding a custom content view API to BiometricPrompt.Builder." bug: "302735104" } - diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 13d5c7e74e4b..6f901d7ec7d2 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -2800,7 +2800,9 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * upright.</p> * <p>Camera devices may either encode this value into the JPEG EXIF header, or * rotate the image data to match this orientation. When the image data is rotated, - * the thumbnail data will also be rotated.</p> + * the thumbnail data will also be rotated. Additionally, in the case where the image data + * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight } + * will not be updated to reflect the height and width of the rotated image.</p> * <p>Note that this orientation is relative to the orientation of the camera sensor, given * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p> * <p>To translate from the device orientation given by the Android sensor APIs for camera diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index 7145501c718d..69b1c34a1da2 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -3091,7 +3091,9 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * upright.</p> * <p>Camera devices may either encode this value into the JPEG EXIF header, or * rotate the image data to match this orientation. When the image data is rotated, - * the thumbnail data will also be rotated.</p> + * the thumbnail data will also be rotated. Additionally, in the case where the image data + * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight } + * will not be updated to reflect the height and width of the rotated image.</p> * <p>Note that this orientation is relative to the orientation of the camera sensor, given * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p> * <p>To translate from the device orientation given by the Android sensor APIs for camera diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java index 5b32f33777fa..c00e6101b363 100644 --- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java @@ -1757,7 +1757,8 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { mCallbacks, result.getSequenceId()); } if ((!mSingleCapture) && (mPreviewProcessorType == - IPreviewExtenderImpl.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY)) { + IPreviewExtenderImpl.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY) + && mInitialized) { CaptureStageImpl captureStage = null; try { captureStage = mPreviewRequestUpdateProcessor.process( @@ -1780,8 +1781,8 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { } else { mRequestUpdatedNeeded = false; } - } else if (mPreviewProcessorType == - IPreviewExtenderImpl.PROCESSOR_TYPE_IMAGE_PROCESSOR) { + } else if ((mPreviewProcessorType == + IPreviewExtenderImpl.PROCESSOR_TYPE_IMAGE_PROCESSOR) && mInitialized) { int idx = mPendingResultMap.indexOfKey(timestamp); if ((idx >= 0) && (mPendingResultMap.get(timestamp).first == null)) { @@ -1828,7 +1829,7 @@ public final class CameraExtensionSessionImpl extends CameraExtensionSession { } else { // No special handling for PROCESSOR_TYPE_NONE } - if (notifyClient) { + if (notifyClient && mInitialized) { final long ident = Binder.clearCallingIdentity(); try { if (processStatus) { diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java index b0f354fac009..3b2913c81d49 100644 --- a/core/java/android/hardware/camera2/params/SessionConfiguration.java +++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java @@ -133,7 +133,7 @@ public final class SessionConfiguration implements Parcelable { * {@link CameraDeviceSetup.isSessionConfigurationSupported} and {@link * CameraDeviceSetup.getSessionCharacteristics} to query a camera device's feature * combination support and session specific characteristics. For the SessionConfiguration - * object to be used to create a capture session, {@link #setCallback} must be called to + * object to be used to create a capture session, {@link #setStateCallback} must be called to * specify the state callback function, and any incomplete OutputConfigurations must be * completed via {@link OutputConfiguration#addSurface} or * {@link OutputConfiguration#setSurfacesForMultiResolutionOutput} as appropriate.</p> @@ -419,7 +419,7 @@ public final class SessionConfiguration implements Parcelable { * @param cb A state callback interface implementation. */ @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) - public void setCallback( + public void setStateCallback( @NonNull @CallbackExecutor Executor executor, @NonNull CameraCaptureSession.StateCallback cb) { mStateCallback = cb; diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java index b067095668b2..978a8f9200ba 100644 --- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java +++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java @@ -1473,6 +1473,11 @@ public final class StreamConfigurationMap { * <li>ImageFormat.DEPTH_JPEG => HAL_DATASPACE_DYNAMIC_DEPTH * <li>ImageFormat.HEIC => HAL_DATASPACE_HEIF * <li>ImageFormat.JPEG_R => HAL_DATASPACE_JPEG_R + * <li>ImageFormat.YUV_420_888 => HAL_DATASPACE_JFIF + * <li>ImageFormat.RAW_SENSOR => HAL_DATASPACE_ARBITRARY + * <li>ImageFormat.RAW_OPAQUE => HAL_DATASPACE_ARBITRARY + * <li>ImageFormat.RAW10 => HAL_DATASPACE_ARBITRARY + * <li>ImageFormat.RAW12 => HAL_DATASPACE_ARBITRARY * <li>others => HAL_DATASPACE_UNKNOWN * </ul> * </p> @@ -1511,6 +1516,11 @@ public final class StreamConfigurationMap { return HAL_DATASPACE_JPEG_R; case ImageFormat.YUV_420_888: return HAL_DATASPACE_JFIF; + case ImageFormat.RAW_SENSOR: + case ImageFormat.RAW_PRIVATE: + case ImageFormat.RAW10: + case ImageFormat.RAW12: + return HAL_DATASPACE_ARBITRARY; default: return HAL_DATASPACE_UNKNOWN; } @@ -2005,6 +2015,12 @@ public final class StreamConfigurationMap { private static final int HAL_DATASPACE_RANGE_SHIFT = 27; private static final int HAL_DATASPACE_UNKNOWN = 0x0; + + /** + * @hide + */ + public static final int HAL_DATASPACE_ARBITRARY = 0x1; + /** @hide */ public static final int HAL_DATASPACE_V0_JFIF = (2 << HAL_DATASPACE_STANDARD_SHIFT) | diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java index b214da227a2d..689e343bcbc6 100644 --- a/core/java/android/hardware/devicestate/DeviceState.java +++ b/core/java/android/hardware/devicestate/DeviceState.java @@ -173,7 +173,7 @@ public final class DeviceState { public static final int PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT = 17; /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN, @@ -197,7 +197,7 @@ public final class DeviceState { public @interface DeviceStateProperties {} /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN @@ -207,7 +207,7 @@ public final class DeviceState { public @interface PhysicalDeviceStateProperties {} /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS, PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP, PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL, diff --git a/core/java/android/hardware/devicestate/feature/flags.aconfig b/core/java/android/hardware/devicestate/feature/flags.aconfig index 73a9e346bd5d..e474603f2b03 100644 --- a/core/java/android/hardware/devicestate/feature/flags.aconfig +++ b/core/java/android/hardware/devicestate/feature/flags.aconfig @@ -2,6 +2,7 @@ package: "android.hardware.devicestate.feature.flags" flag { name: "device_state_property_api" + is_exported: true namespace: "windowing_sdk" description: "Updated DeviceState hasProperty API" bug: "293636629" diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index b0f69f56cba7..81e321d96aa6 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -83,8 +83,7 @@ import javax.crypto.Mac; /** * A class that coordinates access to the fingerprint hardware. - * - * @removed See {@link BiometricPrompt} which shows a system-provided dialog upon starting + * @deprecated See {@link BiometricPrompt} which shows a system-provided dialog upon starting * authentication. In a world where devices may have different types of biometric authentication, * it's much more realistic to have a system-provided authentication dialog since the method may * vary by vendor/device. @@ -95,6 +94,7 @@ import javax.crypto.Mac; @RequiresFeature(PackageManager.FEATURE_FINGERPRINT) public class FingerprintManager implements BiometricAuthenticator, BiometricFingerprintConstants { private static final String TAG = "FingerprintManager"; + private static final boolean DEBUG = true; private static final int MSG_ENROLL_RESULT = 100; private static final int MSG_ACQUIRED = 101; private static final int MSG_AUTHENTICATION_SUCCEEDED = 102; @@ -196,7 +196,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Retrieves a test session for FingerprintManager. - * * @hide */ @TestApi @@ -255,10 +254,9 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } /** - * A wrapper class for the crypto objects supported by FingerprintManager. Currently, the + * A wrapper class for the crypto objects supported by FingerprintManager. Currently the * framework supports {@link Signature}, {@link Cipher} and {@link Mac} objects. - * - * @removed See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} + * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.CryptoObject} */ @Deprecated public static final class CryptoObject extends android.hardware.biometrics.CryptoObject { @@ -332,8 +330,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Container for callback data from {@link FingerprintManager#authenticate(CryptoObject, * CancellationSignal, int, AuthenticationCallback, Handler)}. - * - * @removed See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult} + * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationResult} */ @Deprecated public static class AuthenticationResult { @@ -395,8 +392,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing * FingerprintManager#authenticate(CryptoObject, CancellationSignal, * int, AuthenticationCallback, Handler) } must provide an implementation of this for listening to * fingerprint events. - * - * @removed See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback} + * @deprecated See {@link android.hardware.biometrics.BiometricPrompt.AuthenticationCallback} */ @Deprecated public static abstract class AuthenticationCallback @@ -459,7 +455,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Callback structure provided for {@link #detectFingerprint(CancellationSignal, * FingerprintDetectionCallback, int, Surface)}. - * * @hide */ public interface FingerprintDetectionCallback { @@ -613,8 +608,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing * by <a href="{@docRoot}training/articles/keystore.html">Android Keystore * facility</a>. * @throws IllegalStateException if the crypto primitive is not initialized. - * - * @removed See {@link BiometricPrompt#authenticate(CancellationSignal, Executor, + * @deprecated See {@link BiometricPrompt#authenticate(CancellationSignal, Executor, * BiometricPrompt.AuthenticationCallback)} and {@link BiometricPrompt#authenticate( * BiometricPrompt.CryptoObject, CancellationSignal, Executor, * BiometricPrompt.AuthenticationCallback)} @@ -629,7 +623,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Per-user version of authenticate. * @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FingerprintAuthenticateOptions)}. - * * @hide */ @Deprecated @@ -642,7 +635,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Per-user and per-sensor version of authenticate. * @deprecated use {@link #authenticate(CryptoObject, CancellationSignal, AuthenticationCallback, Handler, FingerprintAuthenticateOptions)}. - * * @hide */ @Deprecated @@ -659,7 +651,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Version of authenticate with additional options. - * * @hide */ @RequiresPermission(anyOf = {USE_BIOMETRIC, USE_FINGERPRINT}) @@ -707,7 +698,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Uses the fingerprint hardware to detect for the presence of a finger, without giving details * about accept/reject/lockout. - * * @hide */ @RequiresPermission(USE_BIOMETRIC_INTERNAL) @@ -750,7 +740,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing * @param callback an object to receive enrollment events * @param shouldLogMetrics a flag that indicates if enrollment failure/success metrics * should be logged. - * * @hide */ @RequiresPermission(MANAGE_FINGERPRINT) @@ -821,7 +810,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Same as {@link #generateChallenge(int, GenerateChallengeCallback)}, but assumes the first * enumerated sensor. - * * @hide */ @RequiresPermission(MANAGE_FINGERPRINT) @@ -836,7 +824,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Revokes the specified challenge. - * * @hide */ @RequiresPermission(MANAGE_FINGERPRINT) @@ -862,7 +849,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing * @param sensorId Sensor ID that this operation takes effect for * @param userId User ID that this operation takes effect for. * @param hardwareAuthToken An opaque token returned by password confirmation. - * * @hide */ @RequiresPermission(RESET_FINGERPRINT_LOCKOUT) @@ -900,7 +886,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Removes all fingerprint templates for the given user. - * * @hide */ @RequiresPermission(MANAGE_FINGERPRINT) @@ -1020,7 +1005,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Forwards BiometricStateListener to FingerprintService * @param listener new BiometricStateListener being added - * * @hide */ public void registerBiometricStateListener(@NonNull BiometricStateListener listener) { @@ -1172,8 +1156,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing } /** - * This is triggered by SideFpsEventHandler. - * + * This is triggered by SideFpsEventHandler * @hide */ @RequiresPermission(USE_BIOMETRIC_INTERNAL) @@ -1186,8 +1169,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing * Determine if there is at least one fingerprint enrolled. * * @return true if at least one fingerprint is enrolled, false otherwise - * - * @removed See {@link BiometricPrompt} and + * @deprecated See {@link BiometricPrompt} and * {@link FingerprintManager#FINGERPRINT_ERROR_NO_FINGERPRINTS} */ @Deprecated @@ -1221,8 +1203,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing * Determine if fingerprint hardware is present and functional. * * @return true if hardware is present and functional, false otherwise. - * - * @removed See {@link BiometricPrompt} and + * @deprecated See {@link BiometricPrompt} and * {@link FingerprintManager#FINGERPRINT_ERROR_HW_UNAVAILABLE} */ @Deprecated @@ -1248,7 +1229,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Get statically configured sensor properties. - * * @hide */ @RequiresPermission(USE_BIOMETRIC_INTERNAL) @@ -1267,7 +1247,6 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing /** * Returns whether the device has a power button fingerprint sensor. * @return boolean indicating whether power button is fingerprint sensor - * * @hide */ public boolean isPowerbuttonFps() { diff --git a/core/java/android/hardware/flags/overlayproperties_flags.aconfig b/core/java/android/hardware/flags/overlayproperties_flags.aconfig index c6a352e0fedf..1165e650f469 100644 --- a/core/java/android/hardware/flags/overlayproperties_flags.aconfig +++ b/core/java/android/hardware/flags/overlayproperties_flags.aconfig @@ -2,6 +2,7 @@ package: "android.hardware.flags" flag { name: "overlayproperties_class_api" + is_exported: true namespace: "core_graphics" description: "public OverlayProperties class, OverlayProperties#supportMixedColorSpaces and Display#getOverlaySupport API" bug: "267234573" diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index e070fe570907..9684e6498bfa 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -27,6 +27,7 @@ flag { flag { namespace: "input_native" name: "pointer_coords_is_resampled_api" + is_exported: true description: "Makes MotionEvent.PointerCoords#isResampled() a public API" bug: "298197511" } @@ -34,6 +35,7 @@ flag { flag { namespace: "input_native" name: "emoji_and_screenshot_keycodes_available" + is_exported: true description: "Add new KeyEvent keycodes for opening Emoji Picker and Taking Screenshots" bug: "315307777" } diff --git a/core/java/android/hardware/radio/Announcement.java b/core/java/android/hardware/radio/Announcement.java index 3ba3ebceeb18..faa103cf1da3 100644 --- a/core/java/android/hardware/radio/Announcement.java +++ b/core/java/android/hardware/radio/Announcement.java @@ -71,7 +71,7 @@ public final class Announcement implements Parcelable { /** * An event called whenever a list of active announcements change. * - * The entire list is sent each time a new announcement appears or any ends broadcasting. + * <p>The entire list is sent each time a new announcement appears or any ends broadcasting. * * @param activeAnnouncements a full list of active announcements */ diff --git a/core/java/android/hardware/radio/ProgramList.java b/core/java/android/hardware/radio/ProgramList.java index c5167dbc7d4c..6146df8b4b1b 100644 --- a/core/java/android/hardware/radio/ProgramList.java +++ b/core/java/android/hardware/radio/ProgramList.java @@ -357,7 +357,7 @@ public final class ProgramList implements AutoCloseable { /** * Constructor of program list filter. * - * Arrays passed to this constructor become owned by this object, do not modify them later. + * <p>Arrays passed to this constructor will be owned by this object, do not modify them. * * @param identifierTypes see getIdentifierTypes() * @param identifiers see getIdentifiers() @@ -438,12 +438,11 @@ public final class ProgramList implements AutoCloseable { /** * Returns the list of identifier types that satisfy the filter. * - * If the program list entry contains at least one identifier of the type - * listed, it satisfies this condition. + * <p>If the program list entry contains at least one identifier of the type + * listed, it satisfies this condition. Empty list means no filtering on + * identifier type. * - * Empty list means no filtering on identifier type. - * - * @return the list of accepted identifier types, must not be modified + * @return the set of accepted identifier types, must not be modified */ public @NonNull Set<Integer> getIdentifierTypes() { return mIdentifierTypes; @@ -452,12 +451,10 @@ public final class ProgramList implements AutoCloseable { /** * Returns the list of identifiers that satisfy the filter. * - * If the program list entry contains at least one listed identifier, - * it satisfies this condition. - * - * Empty list means no filtering on identifier. + * <p>If the program list entry contains at least one listed identifier, + * it satisfies this condition. Empty list means no filtering on identifier. * - * @return the list of accepted identifiers, must not be modified + * @return the set of accepted identifiers, must not be modified */ public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() { return mIdentifiers; @@ -476,7 +473,7 @@ public final class ProgramList implements AutoCloseable { /** * Checks, if updates on entry modifications should be disabled. * - * If true, 'modified' vector of ProgramListChunk must contain list + * <p>If true, 'modified' vector of ProgramListChunk must contain list * additions only. Once the program is added to the list, it's not * updated anymore. */ diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java index 0740374ad8e2..42028f67f400 100644 --- a/core/java/android/hardware/radio/ProgramSelector.java +++ b/core/java/android/hardware/radio/ProgramSelector.java @@ -36,27 +36,31 @@ import java.util.stream.Stream; /** * A set of identifiers necessary to tune to a given station. * - * This can hold various identifiers, like - * - AM/FM frequency - * - HD Radio subchannel - * - DAB channel info + * <p>This can hold various identifiers, like + * <ui> + * <li>AM/FM frequency</li> + * <li>HD Radio subchannel</li> + * <li>DAB channel info</li> + * </ui> * - * The primary ID uniquely identifies a station and can be used for equality + * <p>The primary ID uniquely identifies a station and can be used for equality * check. The secondary IDs are supplementary and can speed up tuning process, * but the primary ID is sufficient (ie. after a full band scan). * - * Two selectors with different secondary IDs, but the same primary ID are + * <p>Two selectors with different secondary IDs, but the same primary ID are * considered equal. In particular, secondary IDs vector may get updated for * an entry on the program list (ie. when a better frequency for a given * station is found). * - * The primaryId of a given programType MUST be of a specific type: - * - AM, FM: RDS_PI if the station broadcasts RDS, AMFM_FREQUENCY otherwise; - * - AM_HD, FM_HD: HD_STATION_ID_EXT; - * - DAB: DAB_SIDECC; - * - DRMO: DRMO_SERVICE_ID; - * - SXM: SXM_SERVICE_ID; - * - VENDOR: VENDOR_PRIMARY. + * <p>The primaryId of a given programType MUST be of a specific type: + * <ui> + * <li>AM, FM: RDS_PI if the station broadcasts RDS, AMFM_FREQUENCY otherwise;</li> + * <li>AM_HD, FM_HD: HD_STATION_ID_EXT;</li> + * <li>DAB: DAB_SIDECC;</li> + * <li>DRMO: DRMO_SERVICE_ID;</li> + * <li>SXM: SXM_SERVICE_ID;</li> + * <li>VENDOR: VENDOR_PRIMARY.</li> + * </ui> * @hide */ @SystemApi @@ -258,10 +262,10 @@ public final class ProgramSelector implements Parcelable { /** * 64bit additional identifier for HD Radio. * - * <p>Due to Station ID abuse, some HD_STATION_ID_EXT identifiers may be not - * globally unique. To provide a best-effort solution, a short version of - * station name may be carried as additional identifier and may be used - * by the tuner hardware to double-check tuning. + * <p>Due to Station ID abuse, some {@link #IDENTIFIER_TYPE_HD_STATION_ID_EXT} + * identifiers may be not globally unique. To provide a best-effort solution, a + * short version of station name may be carried as additional identifier and + * may be used by the tuner hardware to double-check tuning. * * <p>The name is limited to the first 8 A-Z0-9 characters (lowercase * letters must be converted to uppercase). Encoded in little-endian @@ -384,7 +388,7 @@ public final class ProgramSelector implements Parcelable { * The value format is determined by a vendor. * * <p>It must not be used in any other programType than corresponding VENDOR - * type between VENDOR_START and VENDOR_END (eg. identifier type 1015 must + * type between VENDOR_START and VENDOR_END (e.g. identifier type 1015 must * not be used in any program type other than 1015). */ public static final int IDENTIFIER_TYPE_VENDOR_START = PROGRAM_TYPE_VENDOR_START; @@ -435,9 +439,10 @@ public final class ProgramSelector implements Parcelable { /** * Constructor for ProgramSelector. * - * It's not desired to modify selector objects, so all its fields are initialized at creation. + * <p>It's not desired to modify selector objects, so all its fields are initialized at + * creation. * - * Identifier lists must not contain any nulls, but can itself be null to be interpreted + * <p>Identifier lists must not contain any nulls, but can itself be null to be interpreted * as empty list at object creation. * * @param programType type of a radio technology. @@ -492,8 +497,8 @@ public final class ProgramSelector implements Parcelable { /** * Looks up an identifier of a given type (either primary or secondary). * - * If there are multiple identifiers if a given type, then first in order (where primary id is - * before any secondary) is selected. + * <p>If there are multiple identifiers if a given type, then first in order (where primary id + * is before any secondary) is selected. * * @param type type of identifier. * @return identifier value, if found. @@ -510,11 +515,11 @@ public final class ProgramSelector implements Parcelable { /** * Looks up all identifier of a given type (either primary or secondary). * - * Some identifiers may be provided multiple times, for example - * IDENTIFIER_TYPE_AMFM_FREQUENCY for FM Alternate Frequencies. + * <p>Some identifiers may be provided multiple times, for example + * {@link #IDENTIFIER_TYPE_AMFM_FREQUENCY} for FM Alternate Frequencies. * * @param type type of identifier. - * @return a list of identifiers, generated on each call. May be modified. + * @return an array of identifiers, generated on each call. May be modified. */ public @NonNull Identifier[] getAllIds(@IdentifierType int type) { List<Identifier> out = new ArrayList<>(); @@ -543,14 +548,14 @@ public final class ProgramSelector implements Parcelable { /** * Creates an equivalent ProgramSelector with a given secondary identifier preferred. * - * Used to point to a specific physical identifier for technologies that may broadcast the same - * program on different channels. For example, with a DAB program broadcasted over multiple + * <p>Used to point to a specific physical identifier for technologies that may broadcast the + * same program on different channels. For example, with a DAB program broadcasted over multiple * ensembles, the radio hardware may select the one with the strongest signal. The UI may select * preferred ensemble though, so the radio hardware may try to use it in the first place. * - * This is a best-effort hint for the tuner, not a guaranteed behavior. + * <p>This is a best-effort hint for the tuner, not a guaranteed behavior. * - * Setting the given secondary identifier as preferred means filtering out other secondary + * <p>Setting the given secondary identifier as preferred means filtering out other secondary * identifiers of its type and adding it to the list. * * @param preferred preferred secondary identifier @@ -577,7 +582,7 @@ public final class ProgramSelector implements Parcelable { * * @param band the band. * @param frequencyKhz the frequency in kHz. - * @return new ProgramSelector object representing given frequency. + * @return new {@link ProgramSelector} object representing given frequency. * @throws IllegalArgumentException if provided frequency is out of bounds. */ public static @NonNull ProgramSelector createAmFmSelector( @@ -588,13 +593,13 @@ public final class ProgramSelector implements Parcelable { /** * Checks, if a given AM/FM frequency is roughly valid and in correct unit. * - * It does not check the range precisely: it may provide false positives, but not false + * <p>It does not check the range precisely: it may provide false positives, but not false * negatives. In particular, it may be way off for certain regions. - * The main purpose is to avoid passing inproper units, ie. MHz instead of kHz. + * The main purpose is to avoid passing improper units, ie. MHz instead of kHz. * * @param isAm true, if AM, false if FM. * @param frequencyKhz the frequency in kHz. - * @return true, if the frequency is rougly valid. + * @return true, if the frequency is roughly valid. */ private static boolean isValidAmFmFrequency(boolean isAm, int frequencyKhz) { if (isAm) { @@ -607,7 +612,7 @@ public final class ProgramSelector implements Parcelable { /** * Builds new ProgramSelector for AM/FM frequency. * - * This method variant supports HD Radio subchannels, but it's undesirable to + * <p>This method variant supports HD Radio subchannels, but it's undesirable to * select them manually. Instead, the value should be retrieved from program list. * * @param band the band. @@ -741,9 +746,9 @@ public final class ProgramSelector implements Parcelable { }; /** - * A single program identifier component, eg. frequency or channel ID. + * A single program identifier component, e.g. frequency or channel ID. * - * The long value field holds the value in format described in comments for + * <p>The long value field holds the value in format described in comments for * IdentifierType constants. */ public static final class Identifier implements Parcelable { @@ -776,11 +781,11 @@ public final class ProgramSelector implements Parcelable { } /** - * Returns whether this Identifier's type is considered a category when filtering + * Returns whether this identifier's type is considered a category when filtering * ProgramLists for category entries. * * @see ProgramList.Filter#areCategoriesIncluded - * @return False if this identifier's type is not tuneable (e.g. DAB ensemble or + * @return False if this identifier's type is not tunable (e.g. DAB ensemble or * vendor-specified type). True otherwise. */ public boolean isCategoryType() { @@ -791,14 +796,14 @@ public final class ProgramSelector implements Parcelable { /** * Value of an identifier. * - * Its meaning depends on identifier type, ie. for IDENTIFIER_TYPE_AMFM_FREQUENCY type, - * the value is a frequency in kHz. + * <p>Its meaning depends on identifier type, ie. for + * {@link #IDENTIFIER_TYPE_AMFM_FREQUENCY} type, the value is a frequency in kHz. * - * The range of a value depends on its type; it does not always require the whole long + * <p>The range of a value depends on its type; it does not always require the whole long * range. Casting to necessary type (ie. int) without range checking is correct in front-end * code - any range violations are either errors in the framework or in the - * HAL implementation. For example, IDENTIFIER_TYPE_AMFM_FREQUENCY always fits in int, - * as Integer.MAX_VALUE would mean 2.1THz. + * HAL implementation. For example, {@link #IDENTIFIER_TYPE_AMFM_FREQUENCY} always fits in + * int, as {@link Integer#MAX_VALUE} would mean 2.1THz. * * @return value of an identifier. */ diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java index da6c68646820..61854e44287b 100644 --- a/core/java/android/hardware/radio/RadioManager.java +++ b/core/java/android/hardware/radio/RadioManager.java @@ -102,7 +102,7 @@ public class RadioManager { public @interface RadioStatusType{} - // keep in sync with radio_class_t in /system/core/incluse/system/radio.h + // keep in sync with radio_class_t in /system/core/include/system/radio.h /** Radio module class supporting FM (including HD radio) and AM */ public static final int CLASS_AM_FM = 0; /** Radio module class supporting satellite radio */ @@ -154,7 +154,7 @@ public class RadioManager { /** * Forces mono audio stream reception. * - * Analog broadcasts can recover poor reception conditions by jointing + * <p>Analog broadcasts can recover poor reception conditions by jointing * stereo channels into one. Mainly for, but not limited to AM/FM. */ public static final int CONFIG_FORCE_MONO = 1; @@ -176,7 +176,7 @@ public class RadioManager { /** * Forces the digital playback for the supporting radio technology. * - * User may disable digital-analog handover that happens with poor + * <p>User may disable digital-analog handover that happens with poor * reception conditions. With digital forced, the radio will remain silent * instead of switching to analog channel if it's available. This is purely * user choice, it does not reflect the actual state of handover. @@ -185,7 +185,7 @@ public class RadioManager { /** * RDS Alternative Frequencies. * - * If set and the currently tuned RDS station broadcasts on multiple + * <p>If set and the currently tuned RDS station broadcasts on multiple * channels, radio tuner automatically switches to the best available * alternative. */ @@ -193,7 +193,7 @@ public class RadioManager { /** * RDS region-specific program lock-down. * - * Allows user to lock to the current region as they move into the + * <p>Allows user to lock to the current region as they move into the * other region. */ public static final int CONFIG_RDS_REG = 5; @@ -247,11 +247,12 @@ public class RadioManager { @Retention(RetentionPolicy.SOURCE) public @interface ConfigFlag {} - /***************************************************************************** + /** * Lists properties, options and radio bands supported by a given broadcast radio module. - * Each module has a unique ID used to address it when calling RadioManager APIs. - * Module properties are returned by {@link #listModules(List <ModuleProperties>)} method. - ****************************************************************************/ + * + * <p>Each module has a unique ID used to address it when calling RadioManager APIs. + * Module properties are returned by {@link #listModules(List)} method. + */ public static class ModuleProperties implements Parcelable { private final int mId; @@ -315,8 +316,11 @@ public class RadioManager { return set.stream().mapToInt(Integer::intValue).toArray(); } - /** Unique module identifier provided by the native service. - * For use with {@link #openTuner(int, BandConfig, boolean, RadioTuner.Callback, Handler)}. + /** + * Unique module identifier provided by the native service. + * + * <p>or use with + * {@link #openTuner(int, BandConfig, boolean, RadioTuner.Callback, Handler)}. * @return the radio module unique identifier. */ public int getId() { @@ -324,22 +328,24 @@ public class RadioManager { } /** - * Module service (driver) name as registered with HIDL. + * Module service (driver) name as registered with HIDL or AIDL HAL. * @return the module service name. */ public @NonNull String getServiceName() { return mServiceName; } - /** Module class identifier: {@link #CLASS_AM_FM}, {@link #CLASS_SAT}, {@link #CLASS_DT} + /** + * Module class identifier: {@link #CLASS_AM_FM}, {@link #CLASS_SAT}, {@link #CLASS_DT} * @return the radio module class identifier. */ public int getClassId() { return mClassId; } - /** Human readable broadcast radio module implementor - * @return the name of the radio module implementator. + /** + * Human readable broadcast radio module implementor + * @return the name of the radio module implementer. */ public String getImplementor() { return mImplementor; @@ -352,31 +358,38 @@ public class RadioManager { return mProduct; } - /** Human readable broadcast radio module version number + /** + * Human readable broadcast radio module version number * @return the radio module version. */ public String getVersion() { return mVersion; } - /** Radio module serial number. - * Can be used for subscription services. + /** + * Radio module serial number. + * + * <p>This can be used for subscription services. * @return the radio module serial number. */ public String getSerial() { return mSerial; } - /** Number of tuners available. - * This is the number of tuners that can be open simultaneously. + /** + * Number of tuners available. + * + * <p>This is the number of tuners that can be open simultaneously. * @return the number of tuners supported. */ public int getNumTuners() { return mNumTuners; } - /** Number tuner audio sources available. Must be less or equal to getNumTuners(). - * When more than one tuner is supported, one is usually for playback and has one + /** + * Number tuner audio sources available. Must be less or equal to {@link #getNumTuners}. + * + * <p>When more than one tuner is supported, one is usually for playback and has one * associated audio source and the other is for pre scanning and building a * program list. * @return the number of audio sources available. @@ -387,20 +400,24 @@ public class RadioManager { } /** - * Checks, if BandConfig initialization (after {@link RadioManager#openTuner}) + * Checks, if {@link BandConfig} initialization (after {@link RadioManager#openTuner}) * is required to be done before other operations or not. * - * If it is, the client has to wait for {@link RadioTuner.Callback#onConfigurationChanged} - * callback before executing any other operations. Otherwise, such operation will fail - * returning {@link RadioManager#STATUS_INVALID_OPERATION} error code. + * <p>If it is, the client has to wait for + * {@link RadioTuner.Callback#onConfigurationChanged} callback before executing any other + * operations. Otherwise, such operation will fail returning + * {@link RadioManager#STATUS_INVALID_OPERATION} error code. */ public boolean isInitializationRequired() { return mIsInitializationRequired; } - /** {@code true} if audio capture is possible from radio tuner output. - * This indicates if routing to audio devices not connected to the same HAL as the FM radio - * is possible (e.g. to USB) or DAR (Digital Audio Recorder) feature can be implemented. + /** + * {@code true} if audio capture is possible from radio tuner output. + * + * <p>This indicates if routing to audio devices not connected to the same HAL as the FM + * radio is possible (e.g. to USB) or DAR (Digital Audio Recorder) feature can be + * implemented. * @return {@code true} if audio capture is possible, {@code false} otherwise. */ public boolean isCaptureSupported() { @@ -421,8 +438,8 @@ public class RadioManager { /** * Checks, if a given program type is supported by this tuner. * - * If a program type is supported by radio module, it means it can tune - * to ProgramSelector of a given type. + * <p>If a program type is supported by radio module, it means it can tune + * to {@link ProgramSelector} of a given type. * * @return {@code true} if a given program type is supported. */ @@ -433,8 +450,8 @@ public class RadioManager { /** * Checks, if a given program identifier is supported by this tuner. * - * If an identifier is supported by radio module, it means it can use it for - * tuning to ProgramSelector with either primary or secondary Identifier of + * <p>If an identifier is supported by radio module, it means it can use it for + * tuning to {@link ProgramSelector} with either primary or secondary Identifier of * a given type. * * @return {@code true} if a given program type is supported. @@ -446,9 +463,9 @@ public class RadioManager { /** * A frequency table for Digital Audio Broadcasting (DAB). * - * The key is a channel name, i.e. 5A, 7B. + * <p>The key is a channel name, i.e. 5A, 7B. * - * The value is a frequency, in kHz. + * <p>The value is a frequency, in kHz. * * @return a frequency table, or {@code null} if the module doesn't support DAB */ @@ -460,17 +477,18 @@ public class RadioManager { * A map of vendor-specific opaque strings, passed from HAL without changes. * Format of these strings can vary across vendors. * - * It may be used for extra features, that's not supported by a platform, + * <p>It may be used for extra features, that's not supported by a platform, * for example: preset-slots=6; ultra-hd-capable=false. * - * Keys must be prefixed with unique vendor Java-style namespace, - * eg. 'com.somecompany.parameter1'. + * <p>Keys must be prefixed with unique vendor Java-style namespace, + * e.g. 'com.somecompany.parameter1'. */ public @NonNull Map<String, String> getVendorInfo() { return mVendorInfo; } - /** List of descriptors for all bands supported by this module. + /** + * List of descriptors for all bands supported by this module. * @return an array of {@link BandDescriptor}. */ public BandDescriptor[] getBands() { @@ -590,7 +608,9 @@ public class RadioManager { } /** Radio band descriptor: an element in ModuleProperties bands array. - * It is either an instance of {@link FmBandDescriptor} or {@link AmBandDescriptor} */ + * + * <p>It is either an instance of {@link FmBandDescriptor} or {@link AmBandDescriptor} + */ public static class BandDescriptor implements Parcelable { private final int mRegion; @@ -610,16 +630,18 @@ public class RadioManager { mSpacing = spacing; } - /** Region this band applies to. E.g. {@link #REGION_ITU_1} + /** + * Region this band applies to. E.g. {@link #REGION_ITU_1} * @return the region this band is associated to. */ public int getRegion() { return mRegion; } - /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to: + /** + * Band type, e.g. {@link #BAND_FM}. Defines the subclass this descriptor can be cast to: * <ul> - * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li> - * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li> + * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}</li> + * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}</li> * </ul> * @return the band type. */ @@ -645,23 +667,29 @@ public class RadioManager { return mType == BAND_FM || mType == BAND_FM_HD; } - /** Lower band limit expressed in units according to band type. - * Currently all defined band types express channels as frequency in kHz + /** + * Lower band limit expressed in units according to band type. + * + * <p>Currently all defined band types express channels as frequency in kHz. * @return the lower band limit. */ public int getLowerLimit() { return mLowerLimit; } - /** Upper band limit expressed in units according to band type. - * Currently all defined band types express channels as frequency in kHz + /** + * Upper band limit expressed in units according to band type. + * + * <p>Currently all defined band types express channels as frequency in kHz. * @return the upper band limit. */ public int getUpperLimit() { return mUpperLimit; } - /** Channel spacing in units according to band type. - * Currently all defined band types express channels as frequency in kHz - * @return the channel spacing. + /** + * Channel spacing in units according to band type. + * + * <p>Currently all defined band types express channels as frequency in kHz + * @return the channel spacing.</p> */ public int getSpacing() { return mSpacing; @@ -758,9 +786,11 @@ public class RadioManager { } } - /** FM band descriptor + /** + * FM band descriptor * @see #BAND_FM - * @see #BAND_FM_HD */ + * @see #BAND_FM_HD + */ public static class FmBandDescriptor extends BandDescriptor { private final boolean mStereo; private final boolean mRds; @@ -779,19 +809,22 @@ public class RadioManager { mEa = ea; } - /** Stereo is supported + /** + * Stereo is supported * @return {@code true} if stereo is supported, {@code false} otherwise. */ public boolean isStereoSupported() { return mStereo; } - /** RDS or RBDS(if region is ITU2) is supported + /** + * RDS or RBDS(if region is ITU2) is supported * @return {@code true} if RDS or RBDS is supported, {@code false} otherwise. */ public boolean isRdsSupported() { return mRds; } - /** Traffic announcement is supported + /** + * Traffic announcement is supported * @return {@code true} if TA is supported, {@code false} otherwise. */ public boolean isTaSupported() { @@ -804,8 +837,9 @@ public class RadioManager { return mAf; } - /** Emergency Announcement is supported - * @return {@code true} if Emergency annoucement is supported, {@code false} otherwise. + /** + * Emergency Announcement is supported + * @return {@code true} if Emergency announcement is supported, {@code false} otherwise. */ public boolean isEaSupported() { return mEa; @@ -890,8 +924,10 @@ public class RadioManager { } } - /** AM band descriptor. - * @see #BAND_AM */ + /** + * AM band descriptor. + * @see #BAND_AM + */ public static class AmBandDescriptor extends BandDescriptor { private final boolean mStereo; @@ -903,8 +939,9 @@ public class RadioManager { mStereo = stereo; } - /** Stereo is supported - * @return {@code true} if stereo is supported, {@code false} otherwise. + /** + * Stereo is supported + * @return {@code true} if stereo is supported, {@code false} otherwise. */ public boolean isStereoSupported() { return mStereo; @@ -991,39 +1028,47 @@ public class RadioManager { return mDescriptor; } - /** Region this band applies to. E.g. {@link #REGION_ITU_1} - * @return the region associated with this band. + /** + * Region this band applies to. E.g. {@link #REGION_ITU_1} + * @return the region associated with this band. */ public int getRegion() { return mDescriptor.getRegion(); } - /** Band type, e.g {@link #BAND_FM}. Defines the subclass this descriptor can be cast to: + /** + * Band type, e.g. {@link #BAND_FM}. Defines the subclass this descriptor can be cast to: * <ul> - * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}, </li> - * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}, </li> + * <li>{@link #BAND_FM} or {@link #BAND_FM_HD} cast to {@link FmBandDescriptor}</li> + * <li>{@link #BAND_AM} cast to {@link AmBandDescriptor}</li> * </ul> * @return the band type. */ public int getType() { return mDescriptor.getType(); } - /** Lower band limit expressed in units according to band type. - * Currently all defined band types express channels as frequency in kHz - * @return the lower band limit. + /** + * Lower band limit expressed in units according to band type. + * + * <p>Currently all defined band types express channels as frequency in kHz. + * @return the lower band limit. */ public int getLowerLimit() { return mDescriptor.getLowerLimit(); } - /** Upper band limit expressed in units according to band type. - * Currently all defined band types express channels as frequency in kHz - * @return the upper band limit. + /** + * Upper band limit expressed in units according to band type. + * + * <p>Currently all defined band types express channels as frequency in kHz. + * @return the upper band limit. */ public int getUpperLimit() { return mDescriptor.getUpperLimit(); } - /** Channel spacing in units according to band type. - * Currently all defined band types express channels as frequency in kHz - * @return the channel spacing. + /** + * Channel spacing in units according to band type. + * + * <p>Currently all defined band types express channels as frequency in kHz. + * @return the channel spacing. */ public int getSpacing() { return mDescriptor.getSpacing(); @@ -1089,9 +1134,11 @@ public class RadioManager { } } - /** FM band configuration. + /** + * FM band configuration. * @see #BAND_FM - * @see #BAND_FM_HD */ + * @see #BAND_FM_HD + */ public static class FmBandConfig extends BandConfig { private final boolean mStereo; private final boolean mRds; @@ -1119,28 +1166,32 @@ public class RadioManager { mEa = ea; } - /** Get stereo enable state + /** + * Get stereo enable state * @return the enable state. */ public boolean getStereo() { return mStereo; } - /** Get RDS or RBDS(if region is ITU2) enable state + /** + * Get RDS or RBDS(if region is ITU2) enable state * @return the enable state. */ public boolean getRds() { return mRds; } - /** Get Traffic announcement enable state + /** + * Get Traffic announcement enable state * @return the enable state. */ public boolean getTa() { return mTa; } - /** Get Alternate Frequency Switching enable state + /** + * Get Alternate Frequency Switching enable state * @return the enable state. */ public boolean getAf() { @@ -1285,7 +1336,8 @@ public class RadioManager { return config; } - /** Set stereo enable state + /** + * Set stereo enable state * @param state The new enable state. * @return the same Builder instance. */ @@ -1294,7 +1346,8 @@ public class RadioManager { return this; } - /** Set RDS or RBDS(if region is ITU2) enable state + /** + * Set RDS or RBDS(if region is ITU2) enable state * @param state The new enable state. * @return the same Builder instance. */ @@ -1303,7 +1356,8 @@ public class RadioManager { return this; } - /** Set Traffic announcement enable state + /** + * Set Traffic announcement enable state * @param state The new enable state. * @return the same Builder instance. */ @@ -1312,7 +1366,8 @@ public class RadioManager { return this; } - /** Set Alternate Frequency Switching enable state + /** + * Set Alternate Frequency Switching enable state * @param state The new enable state. * @return the same Builder instance. */ @@ -1321,7 +1376,8 @@ public class RadioManager { return this; } - /** Set Emergency Announcement enable state + /** + * Set Emergency Announcement enable state * @param state The new enable state. * @return the same Builder instance. */ @@ -1332,8 +1388,10 @@ public class RadioManager { }; } - /** AM band configuration. - * @see #BAND_AM */ + /** + * AM band configuration. + * @see #BAND_AM + */ public static class AmBandConfig extends BandConfig { private final boolean mStereo; @@ -1349,7 +1407,8 @@ public class RadioManager { mStereo = stereo; } - /** Get stereo enable state + /** + * Get stereo enable state * @return the enable state. */ public boolean getStereo() { @@ -1453,7 +1512,8 @@ public class RadioManager { return config; } - /** Set stereo enable state + /** + * Set stereo enable state * @param state The new enable state. * @return the same Builder instance. */ @@ -1467,7 +1527,8 @@ public class RadioManager { /** Radio program information. */ public static class ProgramInfo implements Parcelable { - // sourced from hardware/interfaces/broadcastradio/2.0/types.hal + // sourced from + // hardware/interfaces/broadcastradio/aidl/android/hardware/broadcastradio/ProgramInfo.aidl private static final int FLAG_LIVE = 1 << 0; private static final int FLAG_MUTED = 1 << 1; private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2; @@ -1521,10 +1582,10 @@ public class RadioManager { /** * Identifier currently used for program selection. * - * This identifier can be used to determine which technology is + * <p>This identifier can be used to determine which technology is * currently being used for reception. * - * Some program selectors contain tuning information for different radio + * <p>Some program selectors contain tuning information for different radio * technologies (i.e. FM RDS and DAB). For example, user may tune using * a ProgramSelector with RDS_PI primary identifier, but the tuner hardware * may choose to use DAB technology to make actual tuning. This identifier @@ -1537,7 +1598,7 @@ public class RadioManager { /** * Identifier currently used by hardware to physically tune to a channel. * - * Some radio technologies broadcast the same program on multiple channels, + * <p>Some radio technologies broadcast the same program on multiple channels, * i.e. with RDS AF the same program may be broadcasted on multiple * alternative frequencies; the same DAB program may be broadcast on * multiple ensembles. This identifier points to the channel to which the @@ -1550,11 +1611,11 @@ public class RadioManager { /** * Primary identifiers of related contents. * - * Some radio technologies provide pointers to other programs that carry + * <p>Some radio technologies provide pointers to other programs that carry * related content (i.e. DAB soft-links). This field is a list of pointers * to other programs on the program list. * - * Please note, that these identifiers does not have to exist on the program + * <p>Please note, that these identifiers does not have to exist on the program * list - i.e. DAB tuner may provide information on FM RDS alternatives * despite not supporting FM RDS. If the system has multiple tuners, another * one may have it on its list. @@ -1563,7 +1624,8 @@ public class RadioManager { return mRelatedContent; } - /** Main channel expressed in units according to band type. + /** + * Main channel expressed in units according to band type. * Currently all defined band types express channels as frequency in kHz * @return the program channel * @deprecated Use {@link ProgramInfo#getSelector} instead. @@ -1578,7 +1640,8 @@ public class RadioManager { } } - /** Sub channel ID. E.g 1 for HD radio HD1 + /** + * Sub channel ID. E.g. 1 for HD radio HD1 * @return the program sub channel * @deprecated Use {@link ProgramInfo#getSelector} instead. */ @@ -1600,14 +1663,16 @@ public class RadioManager { return (mInfoFlags & FLAG_TUNED) != 0; } - /** {@code true} if the received program is stereo + /** + * {@code true} if the received program is stereo * @return {@code true} if stereo, {@code false} otherwise. */ public boolean isStereo() { return (mInfoFlags & FLAG_STEREO) != 0; } - /** {@code true} if the received program is digital (e.g HD radio) + /** + * {@code true} if the received program is digital (e.g. HD radio) * @return {@code true} if digital, {@code false} otherwise. * @deprecated Use {@link ProgramInfo#getLogicallyTunedTo()} instead. */ @@ -1623,8 +1688,9 @@ public class RadioManager { /** * {@code true} if the program is currently playing live stream. - * This may result in a slightly altered reception parameters, - * usually targetted at reduced latency. + * + * <p>This may result in a slightly altered reception parameters, + * usually targeted at reduced latency. */ public boolean isLive() { return (mInfoFlags & FLAG_LIVE) != 0; @@ -1634,7 +1700,8 @@ public class RadioManager { * {@code true} if radio stream is not playing, i.e. due to bad reception * conditions or buffering. In this state volume knob MAY be disabled to * prevent user increasing volume too much. - * It does NOT mean the user has muted audio. + * + * <p>It does NOT mean the user has muted audio. */ public boolean isMuted() { return (mInfoFlags & FLAG_MUTED) != 0; @@ -1688,8 +1755,9 @@ public class RadioManager { } /** Metadata currently received from this station. - * null if no metadata have been received - * @return current meta data received from this program. + * + * @return current meta data received from this program, {@code null} if no metadata have + * been received */ public RadioMetadata getMetadata() { return mMetadata; @@ -1699,11 +1767,11 @@ public class RadioManager { * A map of vendor-specific opaque strings, passed from HAL without changes. * Format of these strings can vary across vendors. * - * It may be used for extra features, that's not supported by a platform, + * <p>It may be used for extra features, that's not supported by a platform, * for example: paid-service=true; bitrate=320kbps. * - * Keys must be prefixed with unique vendor Java-style namespace, - * eg. 'com.somecompany.parameter1'. + * <p>Keys must be prefixed with unique vendor Java-style namespace, + * e.g. 'com.somecompany.parameter1'. */ public @NonNull Map<String, String> getVendorInfo() { return mVendorInfo; @@ -1830,13 +1898,14 @@ public class RadioManager { /** * Open an interface to control a tuner on a given broadcast radio module. - * Optionally selects and applies the configuration passed as "config" argument. + * + * <p>Optionally selects and applies the configuration passed as "config" argument. * @param moduleId radio module identifier {@link ModuleProperties#getId()}. Mandatory. * @param config desired band and configuration to apply when enabling the hardware module. * optional, can be null. * @param withAudio {@code true} to request a tuner with an audio source. * This tuner is intended for live listening or recording or a radio program. - * If {@code false}, the tuner can only be used to retrieve program informations. + * If {@code false}, the tuner can only be used to retrieve program information. * @param callback {@link RadioTuner.Callback} interface. Mandatory. * @param handler the Handler on which the callbacks will be received. * Can be null if default handler is OK. diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java index 67381ec8e829..31880fd405a8 100644 --- a/core/java/android/hardware/radio/RadioMetadata.java +++ b/core/java/android/hardware/radio/RadioMetadata.java @@ -291,7 +291,7 @@ public final class RadioMetadata implements Parcelable { /** * Provides a Clock that can be used to describe time as provided by the Radio. * - * The clock is defined by the seconds since epoch at the UTC + 0 timezone + * <p>The clock time is defined by the seconds since epoch at the UTC + 0 timezone * and timezone offset from UTC + 0 represented in number of minutes. * * @hide @@ -493,16 +493,16 @@ public final class RadioMetadata implements Parcelable { /** * Retrieves an identifier for a bitmap. * - * The format of an identifier is opaque to the application, + * <p>The format of an identifier is opaque to the application, * with a special case of value 0 being invalid. * An identifier for a given image-tuner pair is unique, so an application * may cache images and determine if there is a necessity to fetch them * again - if identifier changes, it means the image has changed. * - * Only bitmap keys may be used with this method: + * <p>Only bitmap keys may be used with this method: * <ul> - * <li>{@link #METADATA_KEY_ICON}</li> - * <li>{@link #METADATA_KEY_ART}</li> + * <li>{@link #METADATA_KEY_ICON}</li> + * <li>{@link #METADATA_KEY_ART}</li> * </ul> * * @param key The key the value is stored under. @@ -537,7 +537,7 @@ public final class RadioMetadata implements Parcelable { * * <p>Only string array keys may be used with this method: * <ul> - * <li>{@link #METADATA_KEY_UFIDS}</li> + * <li>{@link #METADATA_KEY_UFIDS}</li> * </ul> * * @param key The key the value is stored under @@ -667,17 +667,17 @@ public final class RadioMetadata implements Parcelable { * the METADATA_KEYs defined in this class are used they may only be one * of the following: * <ul> - * <li>{@link #METADATA_KEY_RDS_PS}</li> - * <li>{@link #METADATA_KEY_RDS_RT}</li> - * <li>{@link #METADATA_KEY_TITLE}</li> - * <li>{@link #METADATA_KEY_ARTIST}</li> - * <li>{@link #METADATA_KEY_ALBUM}</li> - * <li>{@link #METADATA_KEY_GENRE}</li> - * <li>{@link #METADATA_KEY_COMMENT_SHORT_DESCRIPTION}</li> - * <li>{@link #METADATA_KEY_COMMENT_ACTUAL_TEXT}</li> - * <li>{@link #METADATA_KEY_COMMERCIAL}</li> - * <li>{@link #METADATA_KEY_HD_STATION_NAME_SHORT}</li> - * <li>{@link #METADATA_KEY_HD_STATION_NAME_LONG}</li> + * <li>{@link #METADATA_KEY_RDS_PS}</li> + * <li>{@link #METADATA_KEY_RDS_RT}</li> + * <li>{@link #METADATA_KEY_TITLE}</li> + * <li>{@link #METADATA_KEY_ARTIST}</li> + * <li>{@link #METADATA_KEY_ALBUM}</li> + * <li>{@link #METADATA_KEY_GENRE}</li> + * <li>{@link #METADATA_KEY_COMMENT_SHORT_DESCRIPTION}</li> + * <li>{@link #METADATA_KEY_COMMENT_ACTUAL_TEXT}</li> + * <li>{@link #METADATA_KEY_COMMERCIAL}</li> + * <li>{@link #METADATA_KEY_HD_STATION_NAME_SHORT}</li> + * <li>{@link #METADATA_KEY_HD_STATION_NAME_LONG}</li> * </ul> * * @param key The key for referencing this value @@ -699,10 +699,10 @@ public final class RadioMetadata implements Parcelable { * the METADATA_KEYs defined in this class are used they may only be one * of the following: * <ul> - * <li>{@link #METADATA_KEY_RDS_PI}</li> - * <li>{@link #METADATA_KEY_RDS_PTY}</li> - * <li>{@link #METADATA_KEY_RBDS_PTY}</li> - * <li>{@link #METADATA_KEY_HD_SUBCHANNELS_AVAILABLE}</li> + * <li>{@link #METADATA_KEY_RDS_PI}</li> + * <li>{@link #METADATA_KEY_RDS_PTY}</li> + * <li>{@link #METADATA_KEY_RBDS_PTY}</li> + * <li>{@link #METADATA_KEY_HD_SUBCHANNELS_AVAILABLE}</li> * </ul> * or any bitmap represented by its identifier. * @@ -720,8 +720,8 @@ public final class RadioMetadata implements Parcelable { * if the METADATA_KEYs defined in this class are used they may only be * one of the following: * <ul> - * <li>{@link #METADATA_KEY_ICON}</li> - * <li>{@link #METADATA_KEY_ART}</li> + * <li>{@link #METADATA_KEY_ICON}</li> + * <li>{@link #METADATA_KEY_ART}</li> * </ul> * <p> * @@ -765,7 +765,7 @@ public final class RadioMetadata implements Parcelable { * the METADATA_KEYs defined in this class are used they may only be one * of the following: * <ul> - * <li>{@link #METADATA_KEY_UFIDS}</li> + * <li>{@link #METADATA_KEY_UFIDS}</li> * </ul> * * @param key The key for referencing this value diff --git a/core/java/android/hardware/radio/flags.aconfig b/core/java/android/hardware/radio/flags.aconfig index dbc1a4b21cfb..d0d10c17ee38 100644 --- a/core/java/android/hardware/radio/flags.aconfig +++ b/core/java/android/hardware/radio/flags.aconfig @@ -2,6 +2,7 @@ package: "android.hardware.radio" flag { name: "hd_radio_improved" + is_exported: true namespace: "car_framework" description: "Feature flag for improved HD radio support with less vendor extensions" bug: "280300929" diff --git a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig index 9e487e1a4fc6..fac02ce652b2 100644 --- a/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig +++ b/core/java/android/hardware/usb/flags/system_sw_usb_flags.aconfig @@ -2,6 +2,7 @@ package: "android.hardware.usb.flags" flag { name: "enable_usb_data_compliance_warning" + is_exported: true namespace: "system_sw_usb" description: "Enable USB data compliance warnings when set" bug: "296119135" diff --git a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig index a4956311995c..3dd746c5fad3 100644 --- a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig +++ b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig @@ -2,6 +2,7 @@ package: "android.hardware.usb.flags" flag { name: "enable_is_pd_compliant_api" + is_exported: true namespace: "usb" description: "Feature flag for the api to check if a port is PD compliant" bug: "323470419" @@ -9,6 +10,7 @@ flag { flag { name: "enable_is_mode_change_supported_api" + is_exported: true namespace: "usb" description: "Feature flag for the api to check if a port supports mode change" bug: "323470419" diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java index 83b7edaec72d..6246dd77fd6d 100644 --- a/core/java/android/net/vcn/VcnManager.java +++ b/core/java/android/net/vcn/VcnManager.java @@ -20,10 +20,12 @@ import static java.util.Objects.requireNonNull; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; import android.content.Context; +import android.content.pm.PackageManager; import android.net.LinkProperties; import android.net.NetworkCapabilities; import android.os.Binder; @@ -69,8 +71,13 @@ import java.util.concurrent.Executor; * tasks. In Safe Mode, the system will allow underlying cellular networks to be used as default. * Additionally, during Safe Mode, the VCN will continue to retry the connections, and will * automatically exit Safe Mode if all active tunnels connect successfully. + * + * <p>Apps targeting Android 15 or newer should check the existence of {@link + * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION} before querying the service. If the feature is + * absent, {@link Context#getSystemService} may return null. */ @SystemService(Context.VCN_MANAGEMENT_SERVICE) +@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) public class VcnManager { @NonNull private static final String TAG = VcnManager.class.getSimpleName(); diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig index 97b773ee12ec..e64823af84cb 100644 --- a/core/java/android/net/vcn/flags.aconfig +++ b/core/java/android/net/vcn/flags.aconfig @@ -20,4 +20,18 @@ flag{ namespace: "vcn" description: "Feature flag for enabling network metric monitor" bug: "282996138" +} + +flag{ + name: "validate_network_on_ipsec_loss" + namespace: "vcn" + description: "Trigger network validation when IPsec packet loss exceeds the threshold" + bug: "329139898" +} + +flag{ + name: "evaluate_ipsec_loss_on_lp_nc_change" + namespace: "vcn" + description: "Re-evaluate IPsec packet loss on LinkProperties or NetworkCapabilities change" + bug: "323238888" }
\ No newline at end of file diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index 387eebe0f376..ed4037c7d246 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -18,6 +18,7 @@ package android.os; import static java.util.Objects.requireNonNull; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -31,6 +32,8 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; @@ -53,6 +56,53 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { @VisibleForTesting static final int FLAG_ALLOW_FDS = 1 << 10; + @VisibleForTesting + static final int FLAG_HAS_BINDERS_KNOWN = 1 << 11; + + @VisibleForTesting + static final int FLAG_HAS_BINDERS = 1 << 12; + + + /** + * Status when the Bundle can <b>assert</b> that the underlying Parcel DOES NOT contain + * Binder object(s). + * + * @hide + */ + public static final int STATUS_BINDERS_NOT_PRESENT = 0; + + /** + * Status when the Bundle can <b>assert</b> that there are Binder object(s) in the Parcel. + * + * @hide + */ + public static final int STATUS_BINDERS_PRESENT = 1; + + /** + * Status when the Bundle cannot be checked for Binders and there is no parcelled data + * available to check either. + * <p> This could happen when a Bundle is unparcelled or was never parcelled, and modified such + * that it is not possible to assert if the Bundle has any Binder objects in the current state. + * + * For e.g. calling {@link #putParcelable} or {@link #putBinder} could have added a Binder + * object to the Bundle but it is not possible to assert this fact unless the Bundle is written + * to a Parcel. + * </p> + * + * @hide + */ + public static final int STATUS_BINDERS_UNKNOWN = 2; + + /** @hide */ + @IntDef(flag = true, prefix = {"STATUS_BINDERS_"}, value = { + STATUS_BINDERS_PRESENT, + STATUS_BINDERS_UNKNOWN, + STATUS_BINDERS_NOT_PRESENT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface HasBinderStatus { + } + /** An unmodifiable {@code Bundle} that is always {@link #isEmpty() empty}. */ public static final Bundle EMPTY; @@ -75,7 +125,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { */ public Bundle() { super(); - mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS; + mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS; } /** @@ -111,7 +161,6 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * * @param from The bundle to be copied. * @param deep Whether is a deep or shallow copy. - * * @hide */ Bundle(Bundle from, boolean deep) { @@ -143,7 +192,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { */ public Bundle(ClassLoader loader) { super(loader); - mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS; + mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS; } /** @@ -154,7 +203,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { */ public Bundle(int capacity) { super(capacity); - mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS; + mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS; } /** @@ -180,7 +229,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { */ public Bundle(PersistableBundle b) { super(b); - mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS; + mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS; } /** @@ -292,6 +341,9 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { if ((mFlags & FLAG_HAS_FDS) != 0) { mFlags &= ~FLAG_HAS_FDS_KNOWN; } + if ((mFlags & FLAG_HAS_BINDERS) != 0) { + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; + } } /** @@ -306,13 +358,20 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { bundle.mOwnsLazyValues = false; mMap.putAll(bundle.mMap); - // FD state is now known if and only if both bundles already knew + // FD and Binders state is now known if and only if both bundles already knew if ((bundle.mFlags & FLAG_HAS_FDS) != 0) { mFlags |= FLAG_HAS_FDS; } if ((bundle.mFlags & FLAG_HAS_FDS_KNOWN) == 0) { mFlags &= ~FLAG_HAS_FDS_KNOWN; } + + if ((bundle.mFlags & FLAG_HAS_BINDERS) != 0) { + mFlags |= FLAG_HAS_BINDERS; + } + if ((bundle.mFlags & FLAG_HAS_BINDERS_KNOWN) == 0) { + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; + } } /** @@ -343,6 +402,33 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { return (mFlags & FLAG_HAS_FDS) != 0; } + /** + * Returns a status indicating whether the bundle contains any parcelled Binder objects. + * @hide + */ + public @HasBinderStatus int hasBinders() { + if ((mFlags & FLAG_HAS_BINDERS_KNOWN) != 0) { + if ((mFlags & FLAG_HAS_BINDERS) != 0) { + return STATUS_BINDERS_PRESENT; + } else { + return STATUS_BINDERS_NOT_PRESENT; + } + } + + final Parcel p = mParcelledData; + if (p == null) { + return STATUS_BINDERS_UNKNOWN; + } + if (p.hasBinders()) { + mFlags = mFlags | FLAG_HAS_BINDERS | FLAG_HAS_BINDERS_KNOWN; + return STATUS_BINDERS_PRESENT; + } else { + mFlags = mFlags & ~FLAG_HAS_BINDERS; + mFlags |= FLAG_HAS_BINDERS_KNOWN; + return STATUS_BINDERS_NOT_PRESENT; + } + } + /** {@hide} */ @Override public void putObject(@Nullable String key, @Nullable Object value) { @@ -464,6 +550,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { unparcel(); mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN; + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** @@ -502,6 +589,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { unparcel(); mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN; + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** @@ -517,6 +605,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { unparcel(); mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN; + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** {@hide} */ @@ -525,6 +614,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { unparcel(); mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN; + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** @@ -540,6 +630,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { unparcel(); mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN; + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** @@ -680,6 +771,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { public void putBinder(@Nullable String key, @Nullable IBinder value) { unparcel(); mMap.put(key, value); + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** @@ -697,6 +789,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { public void putIBinder(@Nullable String key, @Nullable IBinder value) { unparcel(); mMap.put(key, value); + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index ccfb6326d941..bcef8153c691 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -475,6 +475,10 @@ public final class Parcel { private static native boolean nativeHasFileDescriptors(long nativePtr); private static native boolean nativeHasFileDescriptorsInRange( long nativePtr, int offset, int length); + + private static native boolean nativeHasBinders(long nativePtr); + private static native boolean nativeHasBindersInRange( + long nativePtr, int offset, int length); @RavenwoodThrow private static native void nativeWriteInterfaceToken(long nativePtr, String interfaceName); @RavenwoodThrow @@ -970,6 +974,34 @@ public final class Parcel { } /** + * Report whether the parcel contains any marshalled IBinder objects. + * + * @throws UnsupportedOperationException if binder kernel driver was disabled or if method was + * invoked in case of Binder RPC protocol. + * @hide + */ + public boolean hasBinders() { + return nativeHasBinders(mNativePtr); + } + + /** + * Report whether the parcel contains any marshalled {@link IBinder} objects in the range + * defined by {@code offset} and {@code length}. + * + * @param offset The offset from which the range starts. Should be between 0 and + * {@link #dataSize()}. + * @param length The length of the range. Should be between 0 and {@link #dataSize()} - {@code + * offset}. + * @return whether there are binders in the range or not. + * @throws IllegalArgumentException if the parameters are out of the permitted ranges. + * + * @hide + */ + public boolean hasBinders(int offset, int length) { + return nativeHasBindersInRange(mNativePtr, offset, length); + } + + /** * Store or read an IBinder interface token in the parcel at the current * {@link #dataPosition}. This is used to validate that the marshalled * transaction is intended for the target interface. This is typically written diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 7020a38ed08a..db06a6ba0ef5 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -48,6 +48,7 @@ import libcore.io.IoUtils; import java.io.FileDescriptor; import java.io.IOException; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.TimeoutException; /** @@ -588,6 +589,8 @@ public class Process { **/ public static final int THREAD_GROUP_RESTRICTED = 7; + /** @hide */ + public static final int SIGNAL_DEFAULT = 0; public static final int SIGNAL_QUIT = 3; public static final int SIGNAL_KILL = 9; public static final int SIGNAL_USR1 = 10; @@ -1437,6 +1440,49 @@ public class Process { sendSignal(pid, SIGNAL_KILL); } + /** + * Check the tgid and tid pair to see if the tid still exists and belong to the tgid. + * + * TOCTOU warning: the status of the tid can change at the time this method returns. This should + * be used in very rare cases such as checking if a (tid, tgid) pair that is known to exist + * recently no longer exists now. As the possibility of the same tid to be reused under the same + * tgid during a short window is rare. And even if it happens the caller logic should be robust + * to handle it without error. + * + * @throws IllegalArgumentException if tgid or tid is not positive. + * @throws SecurityException if the caller doesn't have the permission, this method is expected + * to be used by system process with {@link #SYSTEM_UID} because it + * internally uses tkill(2). + * @throws NoSuchElementException if the Linux process with pid as the tid has exited or it + * doesn't belong to the tgid. + * @hide + */ + public static final void checkTid(int tgid, int tid) + throws IllegalArgumentException, SecurityException, NoSuchElementException { + sendTgSignalThrows(tgid, tid, SIGNAL_DEFAULT); + } + + /** + * Check if the pid still exists. + * + * TOCTOU warning: the status of the pid can change at the time this method returns. This should + * be used in very rare cases such as checking if a pid that belongs to an isolated process of a + * uid known to exist recently no longer exists now. As the possibility of the same pid to be + * reused again under the same uid during a short window is rare. And even if it happens the + * caller logic should be robust to handle it without error. + * + * @throws IllegalArgumentException if pid is not positive. + * @throws SecurityException if the caller doesn't have the permission, this method is expected + * to be used by system process with {@link #SYSTEM_UID} because it + * internally uses kill(2). + * @throws NoSuchElementException if the Linux process with the pid has exited. + * @hide + */ + public static final void checkPid(int pid) + throws IllegalArgumentException, SecurityException, NoSuchElementException { + sendSignalThrows(pid, SIGNAL_DEFAULT); + } + /** @hide */ public static final native int setUid(int uid); @@ -1451,6 +1497,12 @@ public class Process { */ public static final native void sendSignal(int pid, int signal); + private static native void sendSignalThrows(int pid, int signal) + throws IllegalArgumentException, SecurityException, NoSuchElementException; + + private static native void sendTgSignalThrows(int pid, int tgid, int signal) + throws IllegalArgumentException, SecurityException, NoSuchElementException; + /** * @hide * Private impl for avoiding a log message... DO NOT USE without doing diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 84619a0eee2e..f172c3e52415 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -3188,6 +3188,8 @@ public class UserManager { * @return whether the context user can add a private profile. * @hide */ + @TestApi + @FlaggedApi(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE) @RequiresPermission(anyOf = { Manifest.permission.MANAGE_USERS, Manifest.permission.CREATE_USERS}, diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 375d729a4e08..311e99111d04 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -26,6 +26,7 @@ flag { flag { name: "remove_app_profiler_pss_collection" + is_exported: true namespace: "backstage_power" description: "Replaces background PSS collection in AppProfiler with RSS" bug: "297542292" @@ -33,6 +34,7 @@ flag { flag { name: "allow_thermal_headroom_thresholds" + is_exported: true namespace: "game" description: "Enable thermal headroom thresholds API" bug: "288119641" @@ -41,6 +43,7 @@ flag { # This flag guards the private space feature, its APIs, and some of the feature implementations. The flag android.multiuser.Flags.enable_private_space_features exclusively guards all the implementations. flag { name: "allow_private_profile" + is_exported: true namespace: "profile_experiences" description: "Guards a new Private Profile type in UserManager - everything from its setup to config to deletion." bug: "299069460" @@ -49,6 +52,7 @@ flag { flag { name: "bugreport_mode_max_value" + is_exported: true namespace: "telephony" description: "Introduce a constant as maximum value of bugreport mode." bug: "305067125" @@ -56,6 +60,7 @@ flag { flag { name: "adpf_prefer_power_efficiency" + is_exported: true namespace: "game" description: "Guards the ADPF power efficiency API" bug: "288117936" @@ -63,6 +68,7 @@ flag { flag { name: "security_state_service" + is_exported: true namespace: "dynamic_spl" description: "Guards the Security State API." bug: "302189431" @@ -70,6 +76,7 @@ flag { flag { name: "battery_saver_supported_check_api" + is_exported: true namespace: "backstage_power" description: "Guards a new API in PowerManager to check if battery saver is supported or not." bug: "305067031" @@ -77,6 +84,7 @@ flag { flag { name: "adpf_gpu_report_actual_work_duration" + is_exported: true namespace: "game" description: "Guards the ADPF GPU APIs." bug: "284324521" @@ -114,6 +122,7 @@ flag { flag { name: "battery_part_status_api" + is_exported: true namespace: "phoenix" description: "Feature flag for adding Health HAL v3 APIs." is_fixed_read_only: true @@ -122,6 +131,7 @@ flag { flag { name: "storage_lifetime_api" + is_exported: true namespace: "phoenix" description: "Feature flag for adding storage component health APIs." is_fixed_read_only: true @@ -131,6 +141,7 @@ flag { flag { namespace: "system_performance" name: "telemetry_apis_framework_initialization" + is_exported: true description: "Control framework initialization APIs of telemetry APIs feature." is_fixed_read_only: true bug: "324241334" diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig index d485eca7375b..bb0498ed6a78 100644 --- a/core/java/android/os/vibrator/flags.aconfig +++ b/core/java/android/os/vibrator/flags.aconfig @@ -10,6 +10,7 @@ flag { flag { namespace: "haptics" name: "haptics_customization_enabled" + is_exported: true description: "Enables the haptics customization feature" bug: "241918098" } diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 999bc99b6915..2710df2ec982 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -2,6 +2,7 @@ package: "android.permission.flags" flag { name: "device_aware_permission_apis_enabled" + is_exported: true is_fixed_read_only: true namespace: "permissions" description: "enable device aware permission APIs" @@ -10,6 +11,7 @@ flag { flag { name: "voice_activation_permission_apis" + is_exported: true namespace: "permissions" description: "enable voice activation permission APIs" bug: "287264308" @@ -17,6 +19,7 @@ flag { flag { name: "system_server_role_controller_enabled" + is_exported: true is_fixed_read_only: true namespace: "permissions" description: "enable role controller in system server" @@ -25,6 +28,7 @@ flag { flag { name: "set_next_attribution_source" + is_exported: true namespace: "permissions" description: "enable AttributionSource.setNextAttributionSource" bug: "304478648" @@ -32,6 +36,7 @@ flag { flag { name: "should_register_attribution_source" + is_exported: true namespace: "permissions" description: "enable the shouldRegisterAttributionSource API" bug: "305057691" @@ -39,6 +44,7 @@ flag { flag { name: "attribution_source_constructor" + is_exported: true namespace: "permissions" description: "enable AttributionSource(int, int, String, String, IBinder, String[], AttributionSource)" bug: "304478648" @@ -46,6 +52,7 @@ flag { flag { name: "enhanced_confirmation_mode_apis_enabled" + is_exported: true is_fixed_read_only: true namespace: "permissions" description: "enable enhanced confirmation mode apis" @@ -54,6 +61,7 @@ flag { flag { name: "op_enable_mobile_data_by_user" + is_exported: true namespace: "permissions" description: "enables logging of the OP_ENABLE_MOBILE_DATA_BY_USER" bug: "222650148" @@ -61,6 +69,7 @@ flag { flag { name: "factory_reset_prep_permission_apis" + is_exported: true namespace: "wallet_integration" description: "enable Permission PREPARE_FACTORY_RESET." bug: "302016478" @@ -68,6 +77,7 @@ flag { flag { name: "retail_demo_role_enabled" + is_exported: true namespace: "permissions" description: "default retail demo role holder" bug: "274132354" @@ -82,6 +92,7 @@ flag { flag { name: "wallet_role_enabled" + is_exported: true namespace: "wallet_integration" description: "This flag is used to enabled the Wallet Role for all users on the device" bug: "283989236" @@ -114,6 +125,7 @@ flag { flag { name: "get_emergency_role_holder_api_enabled" + is_exported: true is_fixed_read_only: true namespace: "permissions" description: "Enables the getEmergencyRoleHolder API." diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index e26dc73f7172..5bb490336f5d 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -8502,6 +8502,19 @@ public final class Settings { public static final String ACCESSIBILITY_BUTTON_TARGETS = "accessibility_button_targets"; /** + * Setting specifying the accessibility services, shortcut targets or features + * to be toggled via the floating accessibility menu + * + * <p> This is a colon-separated string list which contains the flattened + * {@link ComponentName} and the class name of a system class + * implementing a supported accessibility feature. + * @hide + */ + @Readable + public static final String ACCESSIBILITY_FLOATING_MENU_TARGETS = + "accessibility_floating_menu_targets"; + + /** * Setting specifying the accessibility services, accessibility shortcut targets, * or features to be toggled via a tile in the quick settings panel. * @@ -11090,21 +11103,12 @@ public final class Settings { "assist_long_press_home_enabled"; /** - * Whether press and hold on nav handle can trigger search. + * Whether all entrypoints can trigger search. Replaces individual settings. * * @hide */ - public static final String SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED = - "search_press_hold_nav_handle_enabled"; - - /** - * Whether long-pressing on the home button can trigger search. - * - * @hide - */ - public static final String SEARCH_LONG_PRESS_HOME_ENABLED = - "search_long_press_home_enabled"; - + public static final String SEARCH_ALL_ENTRYPOINTS_ENABLED = + "search_all_entrypoints_enabled"; /** * Whether or not the accessibility data streaming is enbled for the @@ -12395,6 +12399,17 @@ public final class Settings { */ public static final String HIDE_PRIVATESPACE_ENTRY_POINT = "hide_privatespace_entry_point"; + /** + * Whether or not secure windows should be disabled. This only works on debuggable builds. + * + * <p>When this setting is set to a non-zero value, all windows are treated as non-secure. + * Content in windows with {@link android.view.WindowManager.LayoutParams#FLAG_SECURE} will + * appear in screenshots and recordings. + * + * @hide + */ + public static final String DISABLE_SECURE_WINDOWS = "disable_secure_windows"; + /** @hide */ public static final int PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK = 0; /** @hide */ diff --git a/core/java/android/provider/flags.aconfig b/core/java/android/provider/flags.aconfig index ea1ac2793a11..d0cef83390b9 100644 --- a/core/java/android/provider/flags.aconfig +++ b/core/java/android/provider/flags.aconfig @@ -1,7 +1,15 @@ package: "android.provider" flag { + name: "a11y_standalone_fab_enabled" + namespace: "accessibility" + description: "Separating a11y software shortcut and floating a11y button" + bug: "297544054" +} + +flag { name: "system_settings_default" + is_exported: true namespace: "package_manager_service" description: "Enable Settings.System.resetToDefault APIs." bug: "279083734" @@ -9,6 +17,7 @@ flag { flag { name: "user_keys" + is_exported: true namespace: "privacy_infra_policy" description: "This flag controls new E2EE contact keys API" bug: "290696572" @@ -16,7 +25,8 @@ flag { flag { name: "backup_tasks_settings_screen" + is_exported: true namespace: "backstage_power" description: "Add a new settings page for the RUN_BACKUP_JOBS permission." bug: "320563660" -} +}
\ No newline at end of file diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 3c77c44fb3f0..7f5b550c830a 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -10,6 +10,7 @@ flag { flag { name: "fsverity_api" + is_exported: true namespace: "hardware_backed_security" description: "Feature flag for fs-verity API" bug: "285185747" @@ -64,6 +65,7 @@ flag { flag { name: "frp_enforcement" + is_exported: true namespace: "hardware_backed_security" description: "This flag controls whether PDB enforces FRP" bug: "290312729" diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig index 0bae459fefc3..548f8aa8113a 100644 --- a/core/java/android/security/responsible_apis_flags.aconfig +++ b/core/java/android/security/responsible_apis_flags.aconfig @@ -9,6 +9,7 @@ flag { flag { name: "asm_restrictions_enabled" + is_exported: true namespace: "responsible_apis" description: "Enables ASM restrictions for activity starts and finishes" bug: "230590090" @@ -23,6 +24,7 @@ flag { flag { name: "content_uri_permission_apis" + is_exported: true namespace: "responsible_apis" description: "Enables the content URI permission APIs" bug: "293467489" @@ -30,6 +32,7 @@ flag { flag { name: "enforce_intent_filter_match" + is_exported: true namespace: "responsible_apis" description: "Make delivered intents match components' intent filters" bug: "293560872" diff --git a/core/java/android/service/appprediction/flags/flags.aconfig b/core/java/android/service/appprediction/flags/flags.aconfig index c7e47d4b3627..7f9764e82c5d 100644 --- a/core/java/android/service/appprediction/flags/flags.aconfig +++ b/core/java/android/service/appprediction/flags/flags.aconfig @@ -2,6 +2,7 @@ package: "android.service.appprediction.flags" flag { name: "service_features_api" + is_exported: true namespace: "systemui" description: "Guards the new requestServiceFeatures api" bug: "292565550" diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig index d72441f1e4b7..a3eff3becd49 100644 --- a/core/java/android/service/chooser/flags.aconfig +++ b/core/java/android/service/chooser/flags.aconfig @@ -2,6 +2,7 @@ package: "android.service.chooser" flag { name: "chooser_album_text" + is_exported: true namespace: "intentresolver" description: "Flag controlling the album text subtype hint for sharesheet" bug: "323380224" @@ -9,6 +10,7 @@ flag { flag { name: "enable_sharesheet_metadata_extra" + is_exported: true namespace: "intentresolver" description: "This flag enables sharesheet metadata to be displayed to users." bug: "318942069" @@ -16,6 +18,7 @@ flag { flag { name: "chooser_payload_toggling" + is_exported: true namespace: "intentresolver" description: "This flag controls content toggling in Chooser" bug: "302691505" @@ -23,18 +26,8 @@ flag { flag { name: "enable_chooser_result" + is_exported: true namespace: "intentresolver" description: "Provides additional callbacks with information about user actions in ChooserResult" bug: "263474465" } - -flag { - name: "legacy_chooser_pinning_removal" - namespace: "intentresolver" - description: "Removing pinning functionality from the legacy chooser (used by partial screenshare)" - bug: "301068735" - metadata { - purpose: PURPOSE_BUGFIX - } -} - diff --git a/core/java/android/service/controls/flags/flags.aconfig b/core/java/android/service/controls/flags/flags.aconfig index 3a288440d362..197f1bcbc001 100644 --- a/core/java/android/service/controls/flags/flags.aconfig +++ b/core/java/android/service/controls/flags/flags.aconfig @@ -2,6 +2,7 @@ package: "android.service.controls.flags" flag { name: "home_panel_dream" + is_exported: true namespace: "systemui" description: "Enables the home controls dream feature." bug: "298025023" diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig index c5acc2ceb968..35cd3edcafcb 100644 --- a/core/java/android/service/notification/flags.aconfig +++ b/core/java/android/service/notification/flags.aconfig @@ -10,6 +10,7 @@ flag { flag { name: "redact_sensitive_notifications_from_untrusted_listeners" + is_exported: true namespace: "systemui" description: "This flag controls the redacting of sensitive notifications from untrusted NotificationListenerServices" bug: "306271190" @@ -18,6 +19,7 @@ flag { flag { name: "callstyle_callback_api" + is_exported: true namespace: "systemui" description: "Guards the new CallStyleNotificationEventsCallback" bug: "305095040" diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl index 6dbff7185f6f..908ab5f69775 100644 --- a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl +++ b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl @@ -41,7 +41,9 @@ oneway interface IOnDeviceIntelligenceService { void getFeatureDetails(int callerUid, in Feature feature, in IFeatureDetailsCallback featureDetailsCallback); void getReadOnlyFileDescriptor(in String fileName, in AndroidFuture<ParcelFileDescriptor> future); void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback); - void requestFeatureDownload(int callerUid, in Feature feature, in ICancellationSignal cancellationSignal, in IDownloadCallback downloadCallback); + void requestFeatureDownload(int callerUid, in Feature feature, + in AndroidFuture<ICancellationSignal> cancellationSignal, + in IDownloadCallback downloadCallback); void registerRemoteServices(in IRemoteProcessingService remoteProcessingService); void notifyInferenceServiceConnected(); void notifyInferenceServiceDisconnected(); diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl index 799c7545968e..4213a0996e4c 100644 --- a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl +++ b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl @@ -24,6 +24,7 @@ import android.app.ondeviceintelligence.Feature; import android.os.ICancellationSignal; import android.os.PersistableBundle; import android.os.Bundle; +import com.android.internal.infra.AndroidFuture; import android.service.ondeviceintelligence.IRemoteStorageService; import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; @@ -34,13 +35,16 @@ import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; */ oneway interface IOnDeviceSandboxedInferenceService { void registerRemoteStorageService(in IRemoteStorageService storageService); - void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, in ICancellationSignal cancellationSignal, + void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, + in AndroidFuture<ICancellationSignal> cancellationSignal, in ITokenInfoCallback tokenInfoCallback); void processRequest(int callerUid, in Feature feature, in Bundle request, in int requestType, - in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal, + in AndroidFuture<ICancellationSignal> cancellationSignal, + in AndroidFuture<IProcessingSignal> processingSignal, in IResponseCallback callback); void processRequestStreaming(int callerUid, in Feature feature, in Bundle request, in int requestType, - in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal, + in AndroidFuture<ICancellationSignal> cancellationSignal, + in AndroidFuture<IProcessingSignal> processingSignal, in IStreamingResponseCallback callback); void updateProcessingState(in Bundle processingState, in IProcessingUpdateStatusCallback callback); diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java index 93213182d284..86320b801f6c 100644 --- a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java +++ b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java @@ -148,14 +148,18 @@ public abstract class OnDeviceIntelligenceService extends Service { @Override public void requestFeatureDownload(int callerUid, Feature feature, - ICancellationSignal cancellationSignal, + AndroidFuture cancellationSignalFuture, IDownloadCallback downloadCallback) { Objects.requireNonNull(feature); Objects.requireNonNull(downloadCallback); - + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } OnDeviceIntelligenceService.this.onDownloadFeature(callerUid, feature, - CancellationSignal.fromTransport(cancellationSignal), + CancellationSignal.fromTransport(transport), wrapDownloadCallback(downloadCallback)); } diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java index fc7a4c83f82c..96c45eef3731 100644 --- a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java +++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java @@ -122,46 +122,72 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { @Override public void requestTokenInfo(int callerUid, Feature feature, Bundle request, - ICancellationSignal cancellationSignal, + AndroidFuture cancellationSignalFuture, ITokenInfoCallback tokenInfoCallback) { Objects.requireNonNull(feature); Objects.requireNonNull(tokenInfoCallback); + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } OnDeviceSandboxedInferenceService.this.onTokenInfoRequest(callerUid, feature, request, - CancellationSignal.fromTransport(cancellationSignal), + CancellationSignal.fromTransport(transport), wrapTokenInfoCallback(tokenInfoCallback)); } @Override public void processRequestStreaming(int callerUid, Feature feature, Bundle request, - int requestType, ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + int requestType, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IStreamingResponseCallback callback) { Objects.requireNonNull(feature); Objects.requireNonNull(callback); + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } + IProcessingSignal processingSignalTransport = null; + if (processingSignalFuture != null) { + processingSignalTransport = ProcessingSignal.createTransport(); + processingSignalFuture.complete(processingSignalTransport); + } OnDeviceSandboxedInferenceService.this.onProcessRequestStreaming(callerUid, feature, request, requestType, - CancellationSignal.fromTransport(cancellationSignal), - ProcessingSignal.fromTransport(processingSignal), + CancellationSignal.fromTransport(transport), + ProcessingSignal.fromTransport(processingSignalTransport), wrapStreamingResponseCallback(callback)); } @Override public void processRequest(int callerUid, Feature feature, Bundle request, - int requestType, ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + int requestType, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IResponseCallback callback) { Objects.requireNonNull(feature); Objects.requireNonNull(callback); - + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } + IProcessingSignal processingSignalTransport = null; + if (processingSignalFuture != null) { + processingSignalTransport = ProcessingSignal.createTransport(); + processingSignalFuture.complete(processingSignalTransport); + } OnDeviceSandboxedInferenceService.this.onProcessRequest(callerUid, feature, request, requestType, - CancellationSignal.fromTransport(cancellationSignal), - ProcessingSignal.fromTransport(processingSignal), + CancellationSignal.fromTransport(transport), + ProcessingSignal.fromTransport(processingSignalTransport), wrapResponseCallback(callback)); } @@ -206,7 +232,8 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { * Invoked when caller provides a request for a particular feature to be processed in a * streaming manner. The expectation from the implementation is that when processing the * request, - * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to continuously + * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to + * continuously * provide partial Bundle results for the caller to utilize. Optionally the implementation can * provide the complete response in the {@link StreamingProcessingCallback#onResult} upon * processing completion. diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java index 887b5751ffc8..b9f4c3272207 100644 --- a/core/java/android/service/voice/VisualQueryDetectionService.java +++ b/core/java/android/service/voice/VisualQueryDetectionService.java @@ -262,7 +262,6 @@ public abstract class VisualQueryDetectionService extends Service public void onStopDetection() { } - // TODO(b/324341724): Properly deprecate this API. /** * Informs the system that the attention is gained for the interaction intention * {@link VisualQueryAttentionResult#INTERACTION_INTENTION_AUDIO_VISUAL} with @@ -343,7 +342,6 @@ public abstract class VisualQueryDetectionService extends Service } } - // TODO(b/324341724): Properly deprecate this API. /** * Informs the {@link VisualQueryDetector} with the text content being captured about the * query from the audio source. {@code partialQuery} is provided to the diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java index bf8de06fd244..11858e841a8f 100644 --- a/core/java/android/service/voice/VisualQueryDetector.java +++ b/core/java/android/service/voice/VisualQueryDetector.java @@ -301,8 +301,15 @@ public class VisualQueryDetector { } /** - * A class that lets a VoiceInteractionService implementation interact with - * visual query detection APIs. + * A class that lets a VoiceInteractionService implementation interact with visual query + * detection APIs. + * + * Note that methods in this callbacks are not thread-safe so the invocation of each + * methods will have different order from how they are called in the + * {@link VisualQueryDetectionService}. It is expected to pass a single thread executor or a + * serial executor as the callback executor when creating the {@link VisualQueryDetector} + * with {@link VoiceInteractionService#createVisualQueryDetector( + * PersistableBundle, SharedMemory, Executor, Callback)}. */ public interface Callback { @@ -456,7 +463,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onResultDetected"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryDetected(partialResult); + mExecutor.execute(()->mCallback.onQueryDetected(partialResult)); } }); } diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index 306410c9a98b..2f2a6709f50b 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -932,7 +932,10 @@ public class VoiceInteractionService extends Service { * @param sharedMemory The unrestricted data blob to be provided to the * {@link VisualQueryDetectionService}. Use this to provide models or other such data to the * sandboxed process. - * @param callback The callback to notify of detection events. + * @param callback The callback to notify of detection events. Single threaded or sequential + * executors are recommended for the callback are not guaranteed to be executed + * in the order of how they were called from the + * {@link VisualQueryDetectionService}. * @return An instanece of {@link VisualQueryDetector}. * @throws IllegalStateException when there is an existing {@link VisualQueryDetector}, or when * there is a non-trusted hotword detector running. diff --git a/core/java/android/service/voice/flags/flags.aconfig b/core/java/android/service/voice/flags/flags.aconfig index 22e8cddbfdb8..633304b94a5f 100644 --- a/core/java/android/service/voice/flags/flags.aconfig +++ b/core/java/android/service/voice/flags/flags.aconfig @@ -2,6 +2,7 @@ package: "android.service.voice.flags" flag { name: "allow_training_data_egress_from_hds" + is_exported: true namespace: "machine_learning" description: "This flag allows the hotword detection service to egress training data to the default assistant." bug: "296074924" @@ -9,6 +10,7 @@ flag { flag { name: "allow_hotword_bump_egress" + is_exported: true namespace: "machine_learning" description: "This flag allows hotword detection service to egress reason code for hotword bump." bug: "290951024" @@ -16,6 +18,7 @@ flag { flag { name: "allow_foreground_activities_in_on_show" + is_exported: true namespace: "machine_learning" description: "This flag allows providing foreground app component along with onShow args." bug: "319409708" @@ -23,6 +26,7 @@ flag { flag { name: "allow_various_attention_types" + is_exported: true namespace: "visual_query" description: "This flag allows visual query detection service to set different attention types." bug: "318617199" @@ -30,6 +34,7 @@ flag { flag { name: "allow_complex_results_egress_from_vqds" + is_exported: true namespace: "visual_query" description: "This flag allows visual query detection service egress detailed results. " bug: "318617199" @@ -37,6 +42,7 @@ flag { flag { name: "allow_speaker_id_egress" + is_exported: true namespace: "machine_learning" description: "This flag allows hotword detection service and visual query detection service to egress current speaker profile id." bug: "318617199" diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index bbda0684f1d8..f6d197ca4f93 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -102,6 +102,7 @@ import android.view.WindowInsets; import android.view.WindowLayout; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.ScreenCapture; @@ -211,7 +212,7 @@ public abstract class WallpaperService extends Service { * @hide */ @ChangeId - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) public static final long WEAROS_WALLPAPER_HANDLES_SCALING = 272527315L; static final class WallpaperCommand { @@ -459,7 +460,8 @@ public abstract class WallpaperService extends Service { public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, - int syncSeqId, boolean dragResizing) { + int syncSeqId, boolean dragResizing, + @Nullable ActivityWindowInfo activityWindowInfo) { Message msg = mCaller.obtainMessageIO(MSG_WINDOW_RESIZED, reportDraw ? 1 : 0, mergedConfiguration); diff --git a/core/java/android/speech/flags/speech_flags.aconfig b/core/java/android/speech/flags/speech_flags.aconfig index fd8012746a27..fa3359264ab6 100644 --- a/core/java/android/speech/flags/speech_flags.aconfig +++ b/core/java/android/speech/flags/speech_flags.aconfig @@ -2,6 +2,7 @@ package: "android.speech.flags" flag { name: "multilang_extra_launch" + is_exported: true namespace: "machine_learning" description: "Feature flag for adding new extra for multi-lang feature" bug: "312489931" diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index aff1d4a4ee12..8e1ac631cf03 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -10,6 +10,7 @@ flag { flag { name: "new_fonts_fallback_xml" + is_exported: true namespace: "text" description: "Feature flag for deprecating fonts.xml. By setting true for this feature flag, the new font configuration XML, /system/etc/font_fallback.xml is used. The new XML has a new syntax and flexibility of variable font declarations, but it is not compatible with the apps that reads fonts.xml. So, fonts.xml is maintained as a subset of the font_fallback.xml" # Make read only, as it could be used before the Settings provider is initialized. @@ -26,6 +27,7 @@ flag { flag { name: "fix_line_height_for_locale" + is_exported: true namespace: "text" description: "Feature flag that preserve the line height of the TextView and EditText even if the the locale is different from Latin" bug: "303326708" @@ -33,6 +35,7 @@ flag { flag { name: "no_break_no_hyphenation_span" + is_exported: true namespace: "text" description: "A feature flag that adding new spans that prevents line breaking and hyphenation." bug: "283193586" @@ -57,6 +60,7 @@ flag { flag { name: "use_bounds_for_width" + is_exported: true namespace: "text" description: "Feature flag for preventing horizontal clipping." bug: "63938206" @@ -71,6 +75,7 @@ flag { flag { name: "word_style_auto" + is_exported: true namespace: "text" description: "A feature flag that implements line break word style auto." bug: "280005585" @@ -78,6 +83,7 @@ flag { flag { name: "letter_spacing_justification" + is_exported: true namespace: "text" description: "A feature flag that implement inter character justification." bug: "283193133" @@ -121,8 +127,22 @@ flag { } flag { + name: "handwriting_end_of_line_tap" + namespace: "text" + description: "Initiate handwriting when stylus taps at the end of a line in a focused non-empty TextView with the cursor at the end of that line" + bug: "323376217" +} + +flag { name: "handwriting_cursor_position" namespace: "text" description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph." bug: "323376217" } + +flag { + name: "handwriting_unsupported_message" + namespace: "text" + description: "Feature flag for showing error message when user tries stylus handwriting on a text field which doesn't support it" + bug: "297962571" +} diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 29c83509dbf2..f4dadbb0d25a 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -34,7 +34,9 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.Editor; import android.widget.TextView; +import android.widget.Toast; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.lang.ref.WeakReference; @@ -223,24 +225,43 @@ public class HandwritingInitiator { View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY, /* isHover */ false); if (candidateView != null && candidateView.isEnabled()) { - if (candidateView == getConnectedOrFocusedView()) { - if (!mInitiateWithoutConnection && !candidateView.hasFocus()) { + boolean candidateHasFocus = candidateView.hasFocus(); + if (shouldShowHandwritingUnavailableMessageForView(candidateView)) { + int messagesResId = (candidateView instanceof TextView tv + && tv.isAnyPasswordInputType()) + ? R.string.error_handwriting_unsupported_password + : R.string.error_handwriting_unsupported; + Toast.makeText(candidateView.getContext(), messagesResId, + Toast.LENGTH_SHORT).show(); + if (!candidateView.hasFocus()) { + requestFocusWithoutReveal(candidateView); + } + mImm.showSoftInput(candidateView, 0); + mState.mHandled = true; + mState.mShouldInitHandwriting = false; + motionEvent.setAction((motionEvent.getAction() + & MotionEvent.ACTION_POINTER_INDEX_MASK) + | MotionEvent.ACTION_CANCEL); + candidateView.getRootView().dispatchTouchEvent(motionEvent); + } else if (candidateView == getConnectedOrFocusedView()) { + if (!candidateHasFocus) { requestFocusWithoutReveal(candidateView); } startHandwriting(candidateView); } else if (candidateView.getHandwritingDelegatorCallback() != null) { prepareDelegation(candidateView); } else { - if (!mInitiateWithoutConnection) { + if (mInitiateWithoutConnection) { + if (!candidateHasFocus) { + // schedule for view focus. + mState.mPendingFocusedView = new WeakReference<>(candidateView); + requestFocusWithoutReveal(candidateView); + } + } else { mState.mPendingConnectedView = new WeakReference<>(candidateView); - } - if (!candidateView.hasFocus()) { - requestFocusWithoutReveal(candidateView); - } - if (mInitiateWithoutConnection - && updateFocusedView(candidateView, - /* fromTouchEvent */ true)) { - startHandwriting(candidateView); + if (!candidateHasFocus) { + requestFocusWithoutReveal(candidateView); + } } } } @@ -266,6 +287,9 @@ public class HandwritingInitiator { * gained focus. */ public void onDelegateViewFocused(@NonNull View view) { + if (mInitiateWithoutConnection) { + onEditorFocused(view); + } if (view == getConnectedView()) { tryAcceptStylusHandwritingDelegation(view); } @@ -313,6 +337,33 @@ public class HandwritingInitiator { } /** + * Notify HandwritingInitiator that a new editor is focused. + * @param view the view that received focus. + */ + @VisibleForTesting + public void onEditorFocused(@NonNull View view) { + if (!mInitiateWithoutConnection) { + return; + } + + if (!view.isAutoHandwritingEnabled()) { + clearFocusedView(view); + return; + } + + final View focusedView = getFocusedView(); + if (focusedView == view) { + return; + } + updateFocusedView(view); + + if (mState != null && mState.mPendingFocusedView != null + && mState.mPendingFocusedView.get() == view) { + startHandwriting(view); + } + } + + /** * Notify HandwritingInitiator that the InputConnection has closed for the given view. * The caller of this method should guarantee that each onInputConnectionClosed call * is paired with a onInputConnectionCreated call. @@ -359,7 +410,7 @@ public class HandwritingInitiator { * @return {@code true} if handwriting can initiate for given view. */ @VisibleForTesting - public boolean updateFocusedView(@NonNull View view, boolean fromTouchEvent) { + public boolean updateFocusedView(@NonNull View view) { if (!view.shouldInitiateHandwriting()) { mFocusedView = null; return false; @@ -371,9 +422,7 @@ public class HandwritingInitiator { // A new view just gain focus. By default, we should show hover icon for it. mShowHoverIconForConnectedView = true; } - if (!fromTouchEvent && view.isHandwritingDelegate()) { - tryAcceptStylusHandwritingDelegation(view); - } + return true; } @@ -484,6 +533,15 @@ public class HandwritingInitiator { return view.isStylusHandwritingAvailable(); } + private static boolean shouldShowHandwritingUnavailableMessageForView(@NonNull View view) { + return (view instanceof TextView) && !shouldTriggerStylusHandwritingForView(view); + } + + private static boolean shouldTriggerHandwritingOrShowUnavailableMessageForView( + @NonNull View view) { + return (view instanceof TextView) || shouldTriggerStylusHandwritingForView(view); + } + /** * Returns the pointer icon for the motion event, or null if it doesn't specify the icon. * This gives HandwritingInitiator a chance to show the stylus handwriting icon over a @@ -491,7 +549,7 @@ public class HandwritingInitiator { */ public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) { final View hoverView = findHoverView(event); - if (hoverView == null) { + if (hoverView == null || !shouldTriggerStylusHandwritingForView(hoverView)) { return null; } @@ -594,7 +652,7 @@ public class HandwritingInitiator { /** * Given the location of the stylus event, return the best candidate view to initialize - * handwriting mode. + * handwriting mode or show the handwriting unavailable error message. * * @param x the x coordinates of the stylus event, in the coordinates of the window. * @param y the y coordinates of the stylus event, in the coordinates of the window. @@ -610,7 +668,8 @@ public class HandwritingInitiator { Rect handwritingArea = mTempRect; if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea) && isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover) - && shouldTriggerStylusHandwritingForView(connectedOrFocusedView)) { + && shouldTriggerHandwritingOrShowUnavailableMessageForView( + connectedOrFocusedView)) { if (!isHover && mState != null) { mState.mStylusDownWithinEditorBounds = contains(handwritingArea, x, y, 0f, 0f, 0f, 0f); @@ -628,7 +687,7 @@ public class HandwritingInitiator { final View view = viewInfo.getView(); final Rect handwritingArea = viewInfo.getHandwritingArea(); if (!isInHandwritingArea(handwritingArea, x, y, view, isHover) - || !shouldTriggerStylusHandwritingForView(view)) { + || !shouldTriggerHandwritingOrShowUnavailableMessageForView(view)) { continue; } @@ -832,6 +891,12 @@ public class HandwritingInitiator { */ private WeakReference<View> mPendingConnectedView = null; + /** + * A view which has requested focus and is yet to receive it. + * When view receives focus, a handwriting session should be started for the view. + */ + private WeakReference<View> mPendingFocusedView = null; + /** The pointer id of the stylus pointer that is being tracked. */ private final int mStylusPointerId; /** The time stamp when the stylus pointer goes down. */ @@ -856,7 +921,7 @@ public class HandwritingInitiator { /** The helper method to check if the given view is still active for handwriting. */ private static boolean isViewActive(@Nullable View view) { return view != null && view.isAttachedToWindow() && view.isAggregatedVisible() - && view.shouldInitiateHandwriting(); + && view.shouldTrackHandwritingArea(); } private CursorAnchorInfo getCursorAnchorInfoForConnectionless(View view) { diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl index 5ee526e0343d..1c0834fb22b9 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -30,6 +30,7 @@ import android.view.IScrollCaptureResponseListener; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.inputmethod.ImeTracker; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import com.android.internal.os.IResultReceiver; @@ -61,7 +62,8 @@ oneway interface IWindow { void resized(in ClientWindowFrames frames, boolean reportDraw, in MergedConfiguration newMergedConfiguration, in InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, - int syncSeqId, boolean dragResizing); + int syncSeqId, boolean dragResizing, + in @nullable ActivityWindowInfo activityWindowInfo); /** * Called when this window retrieved control over a specified set of insets sources. diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index e126836020b4..3a90841c5327 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -47,6 +47,19 @@ import java.util.List; * {@hide} */ interface IWindowSession { + + /** + * Bundle key to store the latest sync seq id for the relayout configuration. + * @see #relayout + */ + const String KEY_RELAYOUT_BUNDLE_SEQID = "seqid"; + /** + * Bundle key to store the latest ActivityWindowInfo associated with the relayout configuration. + * Will only be set if the relayout window is an activity window. + * @see #relayout + */ + const String KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO = "activity_window_info"; + int addToDisplay(IWindow window, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, int requestedVisibleTypes, out InputChannel outInputChannel, out InsetsState insetsState, @@ -92,7 +105,7 @@ interface IWindowSession { * @param outSurfaceControl Object in which is placed the new display surface. * @param insetsState The current insets state in the system. * @param activeControls Objects which allow controlling {@link InsetsSource}s. - * @param bundle A temporary object to obtain the latest SyncSeqId. + * @param bundle A Bundle to contain the latest SyncSeqId and any extra relayout optional infos. * @return int Result flags, defined in {@link WindowManagerGlobal}. */ int relayout(IWindow window, in WindowManager.LayoutParams attrs, diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index 1920fa3e949d..2fcffd06db62 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -737,15 +737,17 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation @Override public void onIdMatch(InsetsSource source1, InsetsSource source2) { - final @InsetsType int type = source1.getType(); - if ((type & Type.systemBars()) == 0 + final Rect frame1 = source1.getFrame(); + final Rect frame2 = source2.getFrame(); + if (!source1.hasFlags(InsetsSource.FLAG_ANIMATE_RESIZING) + || !source2.hasFlags(InsetsSource.FLAG_ANIMATE_RESIZING) || !source1.isVisible() || !source2.isVisible() - || source1.getFrame().equals(source2.getFrame()) + || frame1.equals(frame2) || frame1.isEmpty() || frame2.isEmpty() || !(Rect.intersects(mFrame, source1.getFrame()) || Rect.intersects(mFrame, source2.getFrame()))) { return; } - mTypes |= type; + mTypes |= source1.getType(); if (mToState == null) { mToState = new InsetsState(); } @@ -877,7 +879,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation return false; } if (DEBUG) Log.d(TAG, "onStateChanged: " + state); - mLastDispatchedState.set(state, true /* copySources */); final InsetsState lastState = new InsetsState(mState, true /* copySources */); updateState(state); @@ -888,10 +889,13 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation true /* excludesInvisibleIme */)) { if (DEBUG) Log.d(TAG, "onStateChanged, notifyInsetsChanged"); mHost.notifyInsetsChanged(); - if (lastState.getDisplayFrame().equals(mState.getDisplayFrame())) { - InsetsState.traverse(lastState, mState, mStartResizingAnimationIfNeeded); + if (mLastDispatchedState.getDisplayFrame().equals(state.getDisplayFrame())) { + // Here compares the raw states instead of the overridden ones because we don't want + // to animate an insets source that its mServerVisible is false. + InsetsState.traverse(mLastDispatchedState, state, mStartResizingAnimationIfNeeded); } } + mLastDispatchedState.set(state, true /* copySources */); return true; } diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index 4ac78f593530..5c10db19b403 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -99,11 +99,17 @@ public class InsetsSource implements Parcelable { */ public static final int FLAG_FORCE_CONSUMING = 1 << 2; + /** + * Controls whether the insets source will play an animation when resizing. + */ + public static final int FLAG_ANIMATE_RESIZING = 1 << 3; + @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, prefix = "FLAG_", value = { FLAG_SUPPRESS_SCRIM, FLAG_INSETS_ROUNDED_CORNER, FLAG_FORCE_CONSUMING, + FLAG_ANIMATE_RESIZING, }) public @interface Flags {} @@ -546,6 +552,9 @@ public class InsetsSource implements Parcelable { if ((flags & FLAG_FORCE_CONSUMING) != 0) { joiner.add("FORCE_CONSUMING"); } + if ((flags & FLAG_ANIMATE_RESIZING) != 0) { + joiner.add("ANIMATE_RESIZING"); + } return joiner.toString(); } diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS index a2f767d002f4..07d05a4ff1ea 100644 --- a/core/java/android/view/OWNERS +++ b/core/java/android/view/OWNERS @@ -75,12 +75,14 @@ per-file View.java = file:/graphics/java/android/graphics/OWNERS per-file View.java = file:/services/core/java/com/android/server/input/OWNERS per-file View.java = file:/services/core/java/com/android/server/wm/OWNERS per-file View.java = file:/core/java/android/view/inputmethod/OWNERS +per-file View.java = file:/core/java/android/text/OWNERS per-file ViewRootImpl.java = file:/services/accessibility/OWNERS per-file ViewRootImpl.java = file:/core/java/android/service/autofill/OWNERS per-file ViewRootImpl.java = file:/graphics/java/android/graphics/OWNERS per-file ViewRootImpl.java = file:/services/core/java/com/android/server/input/OWNERS per-file ViewRootImpl.java = file:/services/core/java/com/android/server/wm/OWNERS per-file ViewRootImpl.java = file:/core/java/android/view/inputmethod/OWNERS +per-file ViewRootImpl.java = file:/core/java/android/text/OWNERS per-file AccessibilityInteractionController.java = file:/services/accessibility/OWNERS per-file OnReceiveContentListener.java = file:/core/java/android/service/autofill/OWNERS per-file OnReceiveContentListener.java = file:/core/java/android/widget/OWNERS diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 4f5b51d04c4b..cfdf8fab05c2 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -2099,6 +2099,65 @@ public final class SurfaceControl implements Parcelable { } } + /** + * Contains information of the idle time of the screen after which the refresh rate is to be + * reduced. + * + * @hide + */ + public static final class IdleScreenRefreshRateConfig { + /** + * The time(in ms) after which the refresh rate is to be reduced. Defaults to -1, which + * means no timeout has been configured for the current conditions + */ + public int timeoutMillis; + + public IdleScreenRefreshRateConfig() { + timeoutMillis = -1; + } + + public IdleScreenRefreshRateConfig(int timeoutMillis) { + this.timeoutMillis = timeoutMillis; + } + + /** + * Checks whether the two objects have the same values. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof IdleScreenRefreshRateConfig) || other == null) { + return false; + } + + IdleScreenRefreshRateConfig + idleScreenRefreshRateConfig = (IdleScreenRefreshRateConfig) other; + return timeoutMillis == idleScreenRefreshRateConfig.timeoutMillis; + } + + @Override + public int hashCode() { + return Objects.hash(timeoutMillis); + } + + @Override + public String toString() { + return "timeoutMillis: " + timeoutMillis; + } + + /** + * Copies the supplied object's values to this object. + */ + public void copyFrom(IdleScreenRefreshRateConfig other) { + if (other != null) { + this.timeoutMillis = other.timeoutMillis; + } + } + } + /** * Contains information about desired display configuration. @@ -2132,6 +2191,15 @@ public final class SurfaceControl implements Parcelable { */ public final RefreshRateRanges appRequestRanges; + /** + * Represents the idle time of the screen after which the associated display's refresh rate + * is to be reduced to preserve power + * Defaults to null, meaning that the device is not configured to have a timeout. + * Timeout value of -1 refers that the current conditions require no timeout + */ + @Nullable + public IdleScreenRefreshRateConfig idleScreenRefreshRateConfig; + public DesiredDisplayModeSpecs() { this.primaryRanges = new RefreshRateRanges(); this.appRequestRanges = new RefreshRateRanges(); @@ -2144,13 +2212,17 @@ public final class SurfaceControl implements Parcelable { } public DesiredDisplayModeSpecs(int defaultMode, boolean allowGroupSwitching, - RefreshRateRanges primaryRanges, RefreshRateRanges appRequestRanges) { + RefreshRateRanges primaryRanges, RefreshRateRanges appRequestRanges, + @Nullable IdleScreenRefreshRateConfig idleScreenRefreshRateConfig) { this.defaultMode = defaultMode; this.allowGroupSwitching = allowGroupSwitching; this.primaryRanges = new RefreshRateRanges(primaryRanges.physical, primaryRanges.render); this.appRequestRanges = new RefreshRateRanges(appRequestRanges.physical, appRequestRanges.render); + this.idleScreenRefreshRateConfig = + (idleScreenRefreshRateConfig == null) ? null : new IdleScreenRefreshRateConfig( + idleScreenRefreshRateConfig.timeoutMillis); } @Override @@ -2165,7 +2237,9 @@ public final class SurfaceControl implements Parcelable { return other != null && defaultMode == other.defaultMode && allowGroupSwitching == other.allowGroupSwitching && primaryRanges.equals(other.primaryRanges) - && appRequestRanges.equals(other.appRequestRanges); + && appRequestRanges.equals(other.appRequestRanges) + && Objects.equals( + idleScreenRefreshRateConfig, other.idleScreenRefreshRateConfig); } @Override @@ -2181,6 +2255,7 @@ public final class SurfaceControl implements Parcelable { allowGroupSwitching = other.allowGroupSwitching; primaryRanges.copyFrom(other.primaryRanges); appRequestRanges.copyFrom(other.appRequestRanges); + copyIdleScreenRefreshRateConfig(other.idleScreenRefreshRateConfig); } @Override @@ -2188,7 +2263,21 @@ public final class SurfaceControl implements Parcelable { return "defaultMode=" + defaultMode + " allowGroupSwitching=" + allowGroupSwitching + " primaryRanges=" + primaryRanges - + " appRequestRanges=" + appRequestRanges; + + " appRequestRanges=" + appRequestRanges + + " idleScreenRefreshRate=" + String.valueOf(idleScreenRefreshRateConfig); + } + + private void copyIdleScreenRefreshRateConfig(IdleScreenRefreshRateConfig other) { + if (idleScreenRefreshRateConfig == null) { + if (other != null) { + idleScreenRefreshRateConfig = + new IdleScreenRefreshRateConfig(other.timeoutMillis); + } + } else if (other == null) { + idleScreenRefreshRateConfig = null; + } else { + idleScreenRefreshRateConfig.copyFrom(other); + } } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 0a75f4e6d731..736e8159c8c6 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -40,10 +40,13 @@ import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout; import static android.view.flags.Flags.sensitiveContentAppProtection; +import static android.view.flags.Flags.toolkitFrameRateBySizeReadOnly; +import static android.view.flags.Flags.toolkitFrameRateDefaultNormalReadOnly; import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision; import static android.view.flags.Flags.toolkitSetFrameRateReadOnly; import static android.view.flags.Flags.viewVelocityApi; import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR; +import static android.view.inputmethod.Flags.initiationWithoutInputConnection; import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS; import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS; @@ -8496,8 +8499,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * hierarchy * @param refocus when propagate is true, specifies whether to request the * root view place new focus + * @hide */ - void clearFocusInternal(View focused, boolean propagate, boolean refocus) { + public void clearFocusInternal(View focused, boolean propagate, boolean refocus) { if ((mPrivateFlags & PFLAG_FOCUSED) != 0) { mPrivateFlags &= ~PFLAG_FOCUSED; clearParentsWantFocus(); @@ -8668,11 +8672,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, onFocusLost(); } else if (hasWindowFocus()) { notifyFocusChangeToImeFocusController(true /* hasFocus */); - - if (mIsHandwritingDelegate) { - ViewRootImpl viewRoot = getViewRootImpl(); - if (viewRoot != null) { + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot != null) { + if (mIsHandwritingDelegate) { viewRoot.getHandwritingInitiator().onDelegateViewFocused(this); + } else if (initiationWithoutInputConnection() && onCheckIsTextEditor()) { + viewRoot.getHandwritingInitiator().onEditorFocused(this); } } } @@ -12695,7 +12700,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (getSystemGestureExclusionRects().isEmpty() && collectPreferKeepClearRects().isEmpty() && collectUnrestrictedPreferKeepClearRects().isEmpty() - && (info.mHandwritingArea == null || !shouldInitiateHandwriting())) { + && (info.mHandwritingArea == null || !shouldTrackHandwritingArea())) { if (info.mPositionUpdateListener != null) { mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener); info.mPositionUpdateListener = null; @@ -13062,7 +13067,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, void updateHandwritingArea() { // If autoHandwritingArea is not enabled, do nothing. - if (!shouldInitiateHandwriting()) return; + if (!shouldTrackHandwritingArea()) return; final AttachInfo ai = mAttachInfo; if (ai != null) { ai.mViewRootImpl.getHandwritingInitiator().updateHandwritingAreasForView(this); @@ -13080,6 +13085,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Returns whether the handwriting initiator should track the handwriting area for this view, + * either to initiate handwriting mode, or to prepare handwriting delegation, or to show the + * handwriting unsupported message. + * @hide + */ + public boolean shouldTrackHandwritingArea() { + return shouldInitiateHandwriting(); + } + + /** * Sets a callback which should be called when a stylus {@link MotionEvent} occurs within this * view's bounds. The callback will be called from the UI thread. * @@ -16691,6 +16706,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, onFocusLost(); } else if ((mPrivateFlags & PFLAG_FOCUSED) != 0) { notifyFocusChangeToImeFocusController(true /* hasFocus */); + ViewRootImpl viewRoot = getViewRootImpl(); + if (viewRoot != null && initiationWithoutInputConnection() && onCheckIsTextEditor()) { + viewRoot.getHandwritingInitiator().onEditorFocused(this); + } } refreshDrawableState(); @@ -33779,9 +33798,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, || heightDp <= FRAME_RATE_NARROW_THRESHOLD || (widthDp <= FRAME_RATE_SMALL_SIZE_THRESHOLD && heightDp <= FRAME_RATE_SMALL_SIZE_THRESHOLD)) { - return FRAME_RATE_CATEGORY_NORMAL | FRAME_RATE_CATEGORY_REASON_SMALL; + int category = toolkitFrameRateBySizeReadOnly() + ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL; + return category | FRAME_RATE_CATEGORY_REASON_SMALL; } else { - return FRAME_RATE_CATEGORY_HIGH | FRAME_RATE_CATEGORY_REASON_LARGE; + int category = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + return category | FRAME_RATE_CATEGORY_REASON_LARGE; } } @@ -33829,8 +33852,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, frameRateCategory = FRAME_RATE_CATEGORY_HIGH | FRAME_RATE_CATEGORY_REASON_REQUESTED; } else { - // invalid frame rate, default to HIGH - frameRateCategory = FRAME_RATE_CATEGORY_HIGH + // invalid frame rate, use default + int category = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + frameRateCategory = category | FRAME_RATE_CATEGORY_REASON_INVALID; } } else { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index cae66720e49e..3c61854c89f0 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -114,7 +114,9 @@ import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodCl import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER; import static com.android.input.flags.Flags.enablePointerChoreographer; +import static com.android.window.flags.Flags.activityWindowInfoFlag; import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay; +import static com.android.window.flags.Flags.setScPropertiesInClient; import android.Manifest; import android.accessibilityservice.AccessibilityService; @@ -233,6 +235,7 @@ import android.view.contentcapture.ContentCaptureSession; import android.view.inputmethod.ImeTracker; import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; +import android.window.ActivityWindowInfo; import android.window.BackEvent; import android.window.ClientWindowFrames; import android.window.CompatOnBackInvokedCallback; @@ -435,13 +438,27 @@ public final class ViewRootImpl implements ViewParent, * Callback for notifying activities. */ public interface ActivityConfigCallback { + /** + * Notifies about override config change and/or move to different display. + * @param overrideConfig New override config to apply to activity. + * @param newDisplayId New display id, {@link Display#INVALID_DISPLAY} if not changed. + */ + default void onConfigurationChanged(@NonNull Configuration overrideConfig, + int newDisplayId) { + // Must override one of the #onConfigurationChanged. + throw new IllegalStateException("Not implemented"); + } /** * Notifies about override config change and/or move to different display. * @param overrideConfig New override config to apply to activity. * @param newDisplayId New display id, {@link Display#INVALID_DISPLAY} if not changed. + * @param activityWindowInfo New ActivityWindowInfo to apply to activity. */ - void onConfigurationChanged(Configuration overrideConfig, int newDisplayId); + default void onConfigurationChanged(@NonNull Configuration overrideConfig, + int newDisplayId, @Nullable ActivityWindowInfo activityWindowInfo) { + onConfigurationChanged(overrideConfig, newDisplayId); + } /** * Notify the corresponding activity about the request to show or hide a camera compat @@ -467,7 +484,7 @@ public final class ViewRootImpl implements ViewParent, * In that case we receive a call back from {@link ActivityThread} and this flag is used to * preserve the initial value. * - * @see #performConfigurationChange(MergedConfiguration, boolean, int) + * @see #performConfigurationChange */ private boolean mForceNextConfigUpdate; @@ -814,6 +831,13 @@ public final class ViewRootImpl implements ViewParent, /** Configurations waiting to be applied. */ private final MergedConfiguration mPendingMergedConfiguration = new MergedConfiguration(); + /** Non-{@code null} if {@link #mActivityConfigCallback} is not {@code null}. */ + @Nullable + private ActivityWindowInfo mPendingActivityWindowInfo; + /** Non-{@code null} if {@link #mActivityConfigCallback} is not {@code null}. */ + @Nullable + private ActivityWindowInfo mLastReportedActivityWindowInfo; + boolean mScrollMayChange; @SoftInputModeFlags int mSoftInputMode; @@ -1260,8 +1284,18 @@ public final class ViewRootImpl implements ViewParent, * Add activity config callback to be notified about override config changes and camera * compat control state updates. */ - public void setActivityConfigCallback(ActivityConfigCallback callback) { + public void setActivityConfigCallback(@Nullable ActivityConfigCallback callback) { mActivityConfigCallback = callback; + if (!activityWindowInfoFlag()) { + return; + } + if (callback == null) { + mPendingActivityWindowInfo = null; + mLastReportedActivityWindowInfo = null; + } else { + mPendingActivityWindowInfo = new ActivityWindowInfo(); + mLastReportedActivityWindowInfo = new ActivityWindowInfo(); + } } public void setOnContentApplyWindowInsetsListener(OnContentApplyWindowInsetsListener listener) { @@ -2096,7 +2130,8 @@ public final class ViewRootImpl implements ViewParent, /** Handles messages {@link #MSG_RESIZED} and {@link #MSG_RESIZED_REPORT}. */ private void handleResized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, - boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing) { + boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing, + @Nullable ActivityWindowInfo activityWindowInfo) { if (!mAdded) { return; } @@ -2114,7 +2149,14 @@ public final class ViewRootImpl implements ViewParent, mInsetsController.onStateChanged(insetsState); final float compatScale = frames.compatScale; final boolean frameChanged = !mWinFrame.equals(frame); - final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration); + final boolean shouldReportActivityWindowInfoChanged = + // Can be null if callbacks is not set + mLastReportedActivityWindowInfo != null + // Can be null if not activity window + && activityWindowInfo != null + && !mLastReportedActivityWindowInfo.equals(activityWindowInfo); + final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration) + || shouldReportActivityWindowInfoChanged; final boolean attachedFrameChanged = !Objects.equals(mTmpFrames.attachedFrame, attachedFrame); final boolean displayChanged = mDisplay.getDisplayId() != displayId; @@ -2133,7 +2175,8 @@ public final class ViewRootImpl implements ViewParent, if (configChanged) { // If configuration changed - notify about that and, maybe, about move to display. performConfigurationChange(mergedConfiguration, false /* force */, - displayChanged ? displayId : INVALID_DISPLAY /* same display */); + displayChanged ? displayId : INVALID_DISPLAY /* same display */, + activityWindowInfo); } else if (displayChanged) { // Moved to display without config change - report last applied one. onMovedToDisplay(displayId, mLastConfigurationFromResources); @@ -3522,6 +3565,16 @@ public final class ViewRootImpl implements ViewParent, mTransaction.setDefaultFrameRateCompatibility(mSurfaceControl, Surface.FRAME_RATE_COMPATIBILITY_NO_VOTE).apply(); } + + if (setScPropertiesInClient()) { + if (surfaceControlChanged || windowAttributesChanged) { + boolean colorSpaceAgnostic = (lp.privateFlags + & WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) + != 0; + mTransaction.setColorSpaceAgnostic(mSurfaceControl, colorSpaceAgnostic) + .apply(); + } + } } if (DEBUG_LAYOUT) Log.v(mTag, "relayout: frame=" + frame.toShortString() @@ -3532,12 +3585,18 @@ public final class ViewRootImpl implements ViewParent, // WindowManagerService has reported back a frame from a configuration not yet // handled by the client. In this case, we need to accept the configuration so we // do not lay out and draw with the wrong configuration. - if (mRelayoutRequested - && !mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration)) { + boolean shouldPerformConfigurationUpdate = + !mPendingMergedConfiguration.equals(mLastReportedMergedConfiguration) + || !Objects.equals(mPendingActivityWindowInfo, + mLastReportedActivityWindowInfo); + if (mRelayoutRequested && shouldPerformConfigurationUpdate) { if (DEBUG_CONFIGURATION) Log.v(mTag, "Visible with new config: " + mPendingMergedConfiguration.getMergedConfiguration()); performConfigurationChange(new MergedConfiguration(mPendingMergedConfiguration), - !mFirst, INVALID_DISPLAY /* same display */); + !mFirst, INVALID_DISPLAY /* same display */, + mPendingActivityWindowInfo != null + ? new ActivityWindowInfo(mPendingActivityWindowInfo) + : null); updatedConfiguration = true; } final boolean updateSurfaceNeeded = mUpdateSurfaceNeeded; @@ -6063,9 +6122,11 @@ public final class ViewRootImpl implements ViewParent, * @param force Flag indicating if we should force apply the config. * @param newDisplayId Id of new display if moved, {@link Display#INVALID_DISPLAY} if not * changed. + * @param activityWindowInfo New activity window info. {@code null} if it is non-app window, or + * this is not a Configuration change to the activity window (global). */ - private void performConfigurationChange(MergedConfiguration mergedConfiguration, boolean force, - int newDisplayId) { + private void performConfigurationChange(@NonNull MergedConfiguration mergedConfiguration, + boolean force, int newDisplayId, @Nullable ActivityWindowInfo activityWindowInfo) { if (mergedConfiguration == null) { throw new IllegalArgumentException("No merged config provided."); } @@ -6105,6 +6166,9 @@ public final class ViewRootImpl implements ViewParent, } mLastReportedMergedConfiguration.setConfiguration(globalConfig, overrideConfig); + if (mLastReportedActivityWindowInfo != null && activityWindowInfo != null) { + mLastReportedActivityWindowInfo.set(activityWindowInfo); + } mForceNextConfigUpdate = force; if (mActivityConfigCallback != null) { @@ -6112,7 +6176,8 @@ public final class ViewRootImpl implements ViewParent, // This basically initiates a round trip to ActivityThread and back, which will ensure // that corresponding activity and resources are updated before updating inner state of // ViewRootImpl. Eventually it will call #updateConfiguration(). - mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId); + mActivityConfigCallback.onConfigurationChanged(overrideConfig, newDisplayId, + activityWindowInfo); } else { // There is no activity callback - update the configuration right away. updateConfiguration(newDisplayId); @@ -6354,13 +6419,15 @@ public final class ViewRootImpl implements ViewParent, final boolean reportDraw = msg.what == MSG_RESIZED_REPORT; final MergedConfiguration mergedConfiguration = (MergedConfiguration) args.arg2; final InsetsState insetsState = (InsetsState) args.arg3; + final ActivityWindowInfo activityWindowInfo = (ActivityWindowInfo) args.arg4; final boolean forceLayout = args.argi1 != 0; final boolean alwaysConsumeSystemBars = args.argi2 != 0; final int displayId = args.argi3; final int syncSeqId = args.argi4; final boolean dragResizing = args.argi5 != 0; handleResized(frames, reportDraw, mergedConfiguration, insetsState, forceLayout, - alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing); + alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing, + activityWindowInfo); args.recycle(); break; } @@ -6504,7 +6571,8 @@ public final class ViewRootImpl implements ViewParent, mLastReportedMergedConfiguration.getOverrideConfiguration()); performConfigurationChange(new MergedConfiguration(mPendingMergedConfiguration), - false /* force */, INVALID_DISPLAY /* same display */); + false /* force */, INVALID_DISPLAY /* same display */, + mLastReportedActivityWindowInfo); } break; case MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST: { setAccessibilityFocus(null, null); @@ -8933,10 +9001,19 @@ public final class ViewRootImpl implements ViewParent, mTempInsets, mTempControls, mRelayoutBundle); mRelayoutRequested = true; - final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid"); + final int maybeSyncSeqId = mRelayoutBundle.getInt( + IWindowSession.KEY_RELAYOUT_BUNDLE_SEQID); if (maybeSyncSeqId > 0) { mSyncSeqId = maybeSyncSeqId; } + if (activityWindowInfoFlag() && mPendingActivityWindowInfo != null) { + final ActivityWindowInfo outInfo = mRelayoutBundle.getParcelable( + IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO, + ActivityWindowInfo.class); + if (outInfo != null) { + mPendingActivityWindowInfo.set(outInfo); + } + } mWinFrameInScreen.set(mTmpFrames.frame); if (mTranslator != null) { mTranslator.translateRectInScreenToAppWindow(mTmpFrames.frame); @@ -9357,6 +9434,10 @@ public final class ViewRootImpl implements ViewParent, + mLastReportedMergedConfiguration); writer.println(innerPrefix + "mLastConfigurationFromResources=" + mLastConfigurationFromResources); + if (mLastReportedActivityWindowInfo != null) { + writer.println(innerPrefix + "mLastReportedActivityWindowInfo=" + + mLastReportedActivityWindowInfo); + } writer.println(innerPrefix + "mIsAmbientMode=" + mIsAmbientMode); writer.println(innerPrefix + "mUnbufferedInputSource=" + Integer.toHexString(mUnbufferedInputSource)); @@ -9570,12 +9651,14 @@ public final class ViewRootImpl implements ViewParent, @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private void dispatchResized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, - boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing) { + boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing, + @Nullable ActivityWindowInfo activityWindowInfo) { Message msg = mHandler.obtainMessage(reportDraw ? MSG_RESIZED_REPORT : MSG_RESIZED); SomeArgs args = SomeArgs.obtain(); args.arg1 = frames; args.arg2 = mergedConfiguration; args.arg3 = insetsState; + args.arg4 = activityWindowInfo; args.argi1 = forceLayout ? 1 : 0; args.argi2 = alwaysConsumeSystemBars ? 1 : 0; args.argi3 = displayId; @@ -11028,7 +11111,7 @@ public final class ViewRootImpl implements ViewParent, public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, - boolean dragResizing) { + boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) { final boolean isFromResizeItem = mIsFromResizeItem; mIsFromResizeItem = false; // Although this is a AIDL method, it will only be triggered in local process through @@ -11047,7 +11130,8 @@ public final class ViewRootImpl implements ViewParent, if (isFromResizeItem && viewAncestor.mHandler.getLooper() == ActivityThread.currentActivityThread().getLooper()) { viewAncestor.handleResized(frames, reportDraw, mergedConfiguration, insetsState, - forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing); + forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing, + activityWindowInfo); return; } // The the parameters from WindowStateResizeItem are already copied. @@ -11059,7 +11143,8 @@ public final class ViewRootImpl implements ViewParent, mergedConfiguration = new MergedConfiguration(mergedConfiguration); } viewAncestor.dispatchResized(frames, reportDraw, mergedConfiguration, insetsState, - forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing); + forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing, + activityWindowInfo); } @Override @@ -12715,7 +12800,10 @@ public final class ViewRootImpl implements ViewParent, } private boolean shouldEnableDvrr() { - return sToolkitSetFrameRateReadOnlyFlagValue && mIsFrameRatePowerSavingsBalanced; + // uncomment this when we are ready for enabling dVRR + // return sToolkitSetFrameRateReadOnlyFlagValue && mIsFrameRatePowerSavingsBalanced; + return false; + } private void checkIdleness() { diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 86fc6f48a145..56667398265e 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -1476,15 +1476,26 @@ public interface WindowManager extends ViewManager { */ @TestApi static boolean hasWindowExtensionsEnabled() { - return HAS_WINDOW_EXTENSIONS_ON_DEVICE - && ActivityTaskManager.supportsMultiWindow(ActivityThread.currentApplication()) - // Since enableWmExtensionsForAllFlag, HAS_WINDOW_EXTENSIONS_ON_DEVICE is now true - // on all devices by default as a build file property. - // Until finishing flag ramp up, only return true when - // ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15 is false, which is set per device by - // OEMs. - && (Flags.enableWmExtensionsForAllFlag() - || !ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15); + if (!Flags.enableWmExtensionsForAllFlag() && ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15) { + // Since enableWmExtensionsForAllFlag, HAS_WINDOW_EXTENSIONS_ON_DEVICE is now true + // on all devices by default as a build file property. + // Until finishing flag ramp up, only return true when + // ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15 is false, which is set per device by + // OEMs. + return false; + } + + if (!HAS_WINDOW_EXTENSIONS_ON_DEVICE) { + return false; + } + + try { + return ActivityTaskManager.supportsMultiWindow(ActivityThread.currentApplication()); + } catch (Exception e) { + // In case the PackageManager is not set up correctly in test. + Log.e("WindowManager", "Unable to read if the device supports multi window", e); + return false; + } } /** diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 22d8ed91d455..73090800f060 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -638,7 +638,7 @@ public class WindowlessWindowManager implements IWindowSession { mTmpConfig.setConfiguration(mConfiguration, mConfiguration); s.mClient.resized(mTmpFrames, false /* reportDraw */, mTmpConfig, state, false /* forceLayout */, false /* alwaysConsumeSystemBars */, s.mDisplayId, - Integer.MAX_VALUE, false /* dragResizing */); + Integer.MAX_VALUE, false /* dragResizing */, null /* activityWindowInfo */); } catch (RemoteException e) { // Too bad } diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 5b99c71f3a8b..91bd4ea0bc87 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -4,6 +4,7 @@ package: "android.view.accessibility" flag { name: "a11y_overlay_callbacks" + is_exported: true namespace: "accessibility" description: "Whether to allow the passing of result callbacks when attaching a11y overlays." bug: "304478691" @@ -26,6 +27,7 @@ flag { flag { namespace: "accessibility" name: "braille_display_hid" + is_exported: true description: "Enables new APIs for an AccessibilityService to communicate with a HID Braille display" bug: "303522222" } @@ -40,6 +42,7 @@ flag { flag { namespace: "accessibility" name: "collection_info_item_counts" + is_exported: true description: "Fields for total items and the number of important for accessibility items in a collection" bug: "302376158" } @@ -61,6 +64,7 @@ flag { flag { namespace: "accessibility" name: "flash_notification_system_api" + is_exported: true description: "Makes flash notification APIs as system APIs for calling from mainline module" bug: "303131332" } @@ -74,6 +78,7 @@ flag { flag { name: "motion_event_observing" + is_exported: true namespace: "accessibility" description: "Allows accessibility services to intercept but not consume motion events from specified sources." bug: "297595990" @@ -82,6 +87,7 @@ flag { flag { namespace: "accessibility" name: "granular_scrolling" + is_exported: true description: "Allow the use of granular scrolling. This allows scrollable nodes to scroll by increments other than a full screen" bug: "302376158" } @@ -103,6 +109,7 @@ flag { flag { namespace: "accessibility" name: "add_type_window_control" + is_exported: true description: "adds new TYPE_WINDOW_CONTROL to AccessibilityWindowInfo for detecting Window Decorations" bug: "320445550" } diff --git a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig index 5d3153c00e8a..4de0f29c60fe 100644 --- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig +++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig @@ -23,6 +23,7 @@ flag { flag { name: "create_accessibility_overlay_app_op_enabled" + is_exported: true namespace: "content_protection" description: "If true, an appop is logged on creation of accessibility overlays." bug: "289081465" @@ -30,6 +31,7 @@ flag { flag { name: "rapid_clear_notifications_by_listener_app_op_enabled" + is_exported: true namespace: "content_protection" description: "If true, an appop is logged when a notification is rapidly cleared by a notification listener." bug: "289080543" @@ -37,6 +39,7 @@ flag { flag { name: "manage_device_policy_enabled" + is_exported: true namespace: "content_protection" description: "If true, the APIs to manage content protection device policy will be enabled." bug: "319477846" diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig index 05cabd56f532..06598b3dfdbd 100644 --- a/core/java/android/view/flags/refresh_rate_flags.aconfig +++ b/core/java/android/view/flags/refresh_rate_flags.aconfig @@ -2,6 +2,7 @@ package: "android.view.flags" flag { name: "view_velocity_api" + is_exported: true namespace: "toolkit" description: "Feature flag for view content velocity api" bug: "293513816" @@ -16,6 +17,7 @@ flag { flag { name: "toolkit_set_frame_rate_read_only" + is_exported: true namespace: "toolkit" description: "Feature flag for toolkit to set frame rate" bug: "293512962" @@ -24,6 +26,7 @@ flag { flag { name: "expected_presentation_time_api" + is_exported: true namespace: "toolkit" description: "Feature flag for using expected presentation time of the Choreographer" bug: "278730197" @@ -31,6 +34,7 @@ flag { flag { name: "expected_presentation_time_read_only" + is_exported: true namespace: "toolkit" description: "Feature flag for using expected presentation time of the Choreographer" bug: "278730197" diff --git a/core/java/android/view/flags/scroll_feedback_flags.aconfig b/core/java/android/view/flags/scroll_feedback_flags.aconfig index d1d871c2dbda..a7c41046b5b4 100644 --- a/core/java/android/view/flags/scroll_feedback_flags.aconfig +++ b/core/java/android/view/flags/scroll_feedback_flags.aconfig @@ -3,6 +3,7 @@ package: "android.view.flags" flag { namespace: "toolkit" name: "scroll_feedback_api" + is_exported: true description: "Enable the scroll feedback APIs" bug: "239594271" } diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig index 6cf89d685963..c482f8be7315 100644 --- a/core/java/android/view/flags/view_flags.aconfig +++ b/core/java/android/view/flags/view_flags.aconfig @@ -28,6 +28,7 @@ flag { flag { name: "sensitive_content_app_protection_api" + is_exported: true namespace: "permissions" description: "This flag controls the new sensitive content protection API," " The API will be used by other ui toolkits (i.e. compose, webview, custom virtual views)." diff --git a/core/java/android/view/flags/window_insets.aconfig b/core/java/android/view/flags/window_insets.aconfig index 201b7ad62f14..bf6df5ca21cf 100644 --- a/core/java/android/view/flags/window_insets.aconfig +++ b/core/java/android/view/flags/window_insets.aconfig @@ -2,6 +2,7 @@ package: "android.view.flags" flag { name: "customizable_window_headers" + is_exported: true namespace: "lse_desktop_experience" description: "Flag to control the caption bar appearance and to fit app content in its empty space" bug: "316387515" diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 985f542c9982..8efb201d08d6 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -3499,10 +3499,6 @@ public final class InputMethodManager { return false; } mServedView = mNextServedView; - if (initiationWithoutInputConnection() && mServedView.isHandwritingDelegate()) { - mServedView.getViewRootImpl().getHandwritingInitiator().onDelegateViewFocused( - mServedView); - } if (mServedInputConnection != null) { mServedInputConnection.finishComposingTextFromImm(); } diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig index 8d3920f8b1da..be74a65046af 100644 --- a/core/java/android/view/inputmethod/flags.aconfig +++ b/core/java/android/view/inputmethod/flags.aconfig @@ -10,6 +10,7 @@ flag { flag { name: "editorinfo_handwriting_enabled" + is_exported: true namespace: "input_method" description: "Feature flag for adding EditorInfo#mStylusHandwritingEnabled" bug: "293898187" @@ -18,6 +19,7 @@ flag { flag { name: "imm_userhandle_hostsidetests" + is_exported: true namespace: "input_method" description: "Feature flag for replacing UserIdInt with UserHandle in some helper IMM functions" bug: "301713309" @@ -26,6 +28,7 @@ flag { flag { name: "concurrent_input_methods" + is_exported: true namespace: "input_method" description: "Feature flag for concurrent multi-session IME" bug: "284527000" @@ -34,6 +37,7 @@ flag { flag { name: "home_screen_handwriting_delegator" + is_exported: true namespace: "input_method" description: "Feature flag for supporting stylus handwriting delegation from RemoteViews on the home screen" bug: "279959705" @@ -49,6 +53,7 @@ flag { flag { name: "use_zero_jank_proxy" + is_exported: true namespace: "input_method" description: "Feature flag for using a proxy that uses async calls to achieve zero jank for IMMS calls." bug: "293640003" @@ -57,6 +62,7 @@ flag { flag { name: "ime_switcher_revamp" + is_exported: true namespace: "input_method" description: "Feature flag for revamping the Input Method Switcher menu" bug: "311791923" @@ -73,6 +79,7 @@ flag { flag { name: "connectionless_handwriting" + is_exported: true namespace: "input_method" description: "Feature flag for connectionless stylus handwriting APIs" bug: "300979854" diff --git a/core/java/android/webkit/flags.aconfig b/core/java/android/webkit/flags.aconfig index 6938b29e78e9..2d834a8b2384 100644 --- a/core/java/android/webkit/flags.aconfig +++ b/core/java/android/webkit/flags.aconfig @@ -2,6 +2,7 @@ package: "android.webkit" flag { name: "update_service_ipc_wrapper" + is_exported: true namespace: "webview" description: "New API: proper wrapper for IWebViewUpdateService" bug: "319292658" diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 0373539c44ea..fbb5116fb82f 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -9733,7 +9733,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return KEY_EVENT_HANDLED; } if (hasFocus()) { - clearFocus(); + clearFocusInternal(null, /* propagate */ true, /* refocus */ false); InputMethodManager imm = getInputMethodManager(); if (imm != null) { imm.hideSoftInputFromView(this, 0); @@ -13118,6 +13118,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } + // At this point, the event is not a long press, otherwise it would be handled above. + if (Flags.handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP + && shouldStartHandwritingForEndOfLineTap(event)) { + InputMethodManager imm = getInputMethodManager(); + if (imm != null) { + imm.startStylusHandwriting(this); + return true; + } + } + final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused(); @@ -13167,6 +13177,46 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * If handwriting is supported, the TextView is already focused and not empty, and the cursor is + * at the end of a line, a stylus tap after the end of the line will trigger handwriting. + */ + private boolean shouldStartHandwritingForEndOfLineTap(MotionEvent actionUpEvent) { + if (!onCheckIsTextEditor() + || !isEnabled() + || !isAutoHandwritingEnabled() + || TextUtils.isEmpty(mText) + || didTouchFocusSelect() + || mLayout == null + || !actionUpEvent.isStylusPointer()) { + return false; + } + int cursorOffset = getSelectionStart(); + if (cursorOffset < 0 || getSelectionEnd() != cursorOffset) { + return false; + } + int cursorLine = mLayout.getLineForOffset(cursorOffset); + int cursorLineEnd = mLayout.getLineEnd(cursorLine); + if (cursorLine != mLayout.getLineCount() - 1) { + cursorLineEnd--; + } + if (cursorLineEnd != cursorOffset) { + return false; + } + // Check that the stylus down point is within the same line as the cursor. + if (getLineAtCoordinate(actionUpEvent.getY()) != cursorLine) { + return false; + } + // Check that the stylus down point is after the end of the line. + float localX = convertToLocalHorizontalCoordinate(actionUpEvent.getX()); + if (mLayout.getParagraphDirection(cursorLine) == Layout.DIR_RIGHT_TO_LEFT + ? localX >= mLayout.getLineLeft(cursorLine) + : localX <= mLayout.getLineRight(cursorLine)) { + return false; + } + return isStylusHandwritingAvailable(); + } + + /** * Returns true when need to show UIs, e.g. floating toolbar, etc, for finger based interaction. * * @return true if UIs need to show for finger interaciton. false if UIs are not necessary. @@ -13565,6 +13615,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** @hide */ @Override + public boolean shouldTrackHandwritingArea() { + // The handwriting initiator tracks all editable TextViews regardless of whether handwriting + // is supported, so that it can show an error message for unsupported editable TextViews. + return super.shouldTrackHandwritingArea() + || (Flags.handwritingUnsupportedMessage() && onCheckIsTextEditor()); + } + + /** @hide */ + @Override public boolean isStylusHandwritingAvailable() { if (mTextOperationUser == null) { return super.isStylusHandwritingAvailable(); diff --git a/core/java/android/window/InputTransferToken.java b/core/java/android/window/InputTransferToken.java index 5fab48f93316..d2cefa8e0570 100644 --- a/core/java/android/window/InputTransferToken.java +++ b/core/java/android/window/InputTransferToken.java @@ -57,6 +57,7 @@ public final class InputTransferToken implements Parcelable { private static native void nativeWriteToParcel(long nativeObject, Parcel out); private static native long nativeReadFromParcel(Parcel in); private static native IBinder nativeGetBinderToken(long nativeObject); + private static native long nativeGetBinderTokenRef(long nativeObject); private static native long nativeGetNativeInputTransferTokenFinalizer(); private static native boolean nativeEquals(long nativeObject1, long nativeObject2); @@ -130,7 +131,7 @@ public final class InputTransferToken implements Parcelable { */ @Override public int hashCode() { - return Objects.hash(getToken()); + return Objects.hash(nativeGetBinderTokenRef(mNativeObject)); } /** diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index 7e77f150b63b..43df4f962256 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -112,10 +112,13 @@ public final class TaskFragmentOperation implements Parcelable { /** * Creates a decor surface in the parent Task of the TaskFragment. The created decor surface * will be provided in {@link TaskFragmentTransaction#TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED} - * event callback. The decor surface can be used to draw the divider between TaskFragments or - * other decorations. + * event callback. If a decor surface already exists in the parent Task, the current + * TaskFragment will become the new owner of the decor surface and the decor surface will be + * moved above the TaskFragment. + * + * The decor surface can be used to draw the divider between TaskFragments or other decorations. */ - public static final int OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE = 14; + public static final int OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE = 14; /** * Removes the decor surface in the parent Task of the TaskFragment. @@ -162,7 +165,7 @@ public final class TaskFragmentOperation implements Parcelable { OP_TYPE_SET_ISOLATED_NAVIGATION, OP_TYPE_REORDER_TO_BOTTOM_OF_TASK, OP_TYPE_REORDER_TO_TOP_OF_TASK, - OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE, + OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_SET_DIM_ON_TASK, OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH, diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 3685bbabf4d3..5227724e705e 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -542,6 +542,9 @@ public final class TransitionInfo implements Parcelable { // independent either. if (change.getMode() == TRANSIT_CHANGE) return false; + // Always fold the activity embedding change into the parent change. + if (change.hasFlags(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) return false; + TransitionInfo.Change parentChg = info.getChange(change.getParent()); while (parentChg != null) { // If the parent is a visibility change, it will include the results of all child diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index 7f5331b936e9..4a3aba13fd54 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -165,7 +165,8 @@ public class WindowTokenClient extends Binder { Log.d(TAG, "Configuration not dispatch to IME because configuration is up" + " to date. Current config=" + context.getResources().getConfiguration() + ", reported config=" + currentConfig - + ", updated config=" + newConfig); + + ", updated config=" + newConfig + + ", updated display ID=" + newDisplayId); } // Update display first. In case callers want to obtain display information( // ex: DisplayMetrics) in #onConfigurationChanged callback. @@ -190,13 +191,18 @@ public class WindowTokenClient extends Binder { if (mShouldDumpConfigForIme) { if (!shouldReportConfigChange) { Log.d(TAG, "Only apply configuration update to Resources because " - + "shouldReportConfigChange is false.\n" + Debug.getCallers(5)); + + "shouldReportConfigChange is false. " + + "context=" + context + + ", config=" + context.getResources().getConfiguration() + + ", display ID=" + context.getDisplayId() + "\n" + + Debug.getCallers(5)); } else if (diff == 0) { Log.d(TAG, "Configuration not dispatch to IME because configuration has no " + " public difference with updated config. " + " Current config=" + context.getResources().getConfiguration() + ", reported config=" + currentConfig - + ", updated config=" + newConfig); + + ", updated config=" + newConfig + + ", display ID=" + context.getDisplayId()); } } } diff --git a/core/java/android/window/flags/accessibility.aconfig b/core/java/android/window/flags/accessibility.aconfig index 814c62017391..590e88ba11f0 100644 --- a/core/java/android/window/flags/accessibility.aconfig +++ b/core/java/android/window/flags/accessibility.aconfig @@ -12,4 +12,17 @@ flag { namespace: "accessibility" description: "Always draw fullscreen orange border in fullscreen magnification" bug: "291891390" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "use_window_original_touchable_region_when_magnification_recompute_bounds" + namespace: "accessibility" + description: "The flag controls whether to use the window original touchable regions in accessibilityController recomputeBounds" + bug: "323366243" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index 254f4f77c100..7fbec67ec4e9 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -18,6 +18,7 @@ flag { flag { name: "density_390_api" + is_exported: true namespace: "large_screen_experiences_app_compat" description: "Whether the API DisplayMetrics.DENSITY_390 is available" bug: "297550533" @@ -26,6 +27,7 @@ flag { flag { name: "app_compat_properties_api" + is_exported: true namespace: "large_screen_experiences_app_compat" description: "Whether app compat property APIs are public. Which includes: /n" "WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE,/n" diff --git a/core/java/android/window/flags/wallpaper_manager.aconfig b/core/java/android/window/flags/wallpaper_manager.aconfig index ea9da96496c7..dea9497dd624 100644 --- a/core/java/android/window/flags/wallpaper_manager.aconfig +++ b/core/java/android/window/flags/wallpaper_manager.aconfig @@ -2,6 +2,7 @@ package: "com.android.window.flags" flag { name: "always_update_wallpaper_permission" + is_exported: true namespace: "wear_frameworks" description: "Allow out of focus process to update wallpaper complications" bug: "271132915" diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index 3f483418c6b3..5c310484eff9 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -45,6 +45,7 @@ flag { flag { namespace: "window_surfaces" name: "trusted_presentation_listener_for_window" + is_exported: true description: "Enable trustedPresentationListener on windows public API" is_fixed_read_only: true bug: "278027319" @@ -53,6 +54,7 @@ flag { flag { namespace: "window_surfaces" name: "sdk_desired_present_time" + is_exported: true description: "Feature flag for the new SDK API to set desired present time" is_fixed_read_only: true bug: "295038072" @@ -61,6 +63,7 @@ flag { flag { namespace: "window_surfaces" name: "surface_control_input_receiver" + is_exported: true description: "Enable public API to register an InputReceiver for a SurfaceControl" is_fixed_read_only: true bug: "278757236" @@ -69,6 +72,7 @@ flag { flag { namespace: "window_surfaces" name: "screen_recording_callbacks" + is_exported: true description: "Enable screen recording callbacks public API" is_fixed_read_only: true bug: "304574518" @@ -92,3 +96,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "window_surfaces" + name: "set_sc_properties_in_client" + description: "Set VRI SC properties in the client instead of system server" + is_fixed_read_only: true + bug: "308662081" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 14fb17c09031..a5c209db9f5c 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -16,6 +16,7 @@ flag { flag { name: "enforce_edge_to_edge" + is_exported: true namespace: "windowing_frontend" description: "Make app go edge-to-edge when targeting SDK level 35 or greater" bug: "309578419" @@ -31,6 +32,17 @@ flag { } flag { + name: "remove_prepare_surface_in_placement" + namespace: "windowing_frontend" + description: "Reduce unnecessary invocation to improve performance" + bug: "330721336" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "close_to_square_config_includes_status_bar" namespace: "windowing_frontend" description: "On close to square display, when necessary, configuration includes status bar" @@ -38,6 +50,17 @@ flag { } flag { + name: "skip_sleeping_when_switching_display" + namespace: "windowing_frontend" + description: "Reduce unnecessary visibility or lifecycle changes when changing fold state" + bug: "303241079" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "introduce_smoother_dimmer" namespace: "windowing_frontend" description: "Refactor dim to fix flickers" @@ -77,6 +100,7 @@ flag { flag { name: "supports_multi_instance_system_ui" + is_exported: true namespace: "multitasking" description: "Feature flag to enable a multi-instance system ui component property." bug: "262864589" @@ -85,6 +109,7 @@ flag { flag { name: "delegate_unhandled_drags" + is_exported: true namespace: "multitasking" description: "Enables delegating unhandled drags to SystemUI" bug: "320797628" diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index 82e613e18d41..4b3d8e809eca 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -43,6 +43,7 @@ flag { flag { namespace: "windowing_sdk" name: "untrusted_embedding_any_app_permission" + is_exported: true description: "Feature flag to enable the permission to embed any app in untrusted mode." bug: "293647332" is_fixed_read_only: true @@ -59,6 +60,7 @@ flag { flag { namespace: "windowing_sdk" name: "untrusted_embedding_state_sharing" + is_exported: true description: "Feature flag to enable state sharing in untrusted embedding when apps opt in." bug: "293647332" is_fixed_read_only: true @@ -74,6 +76,7 @@ flag { flag { namespace: "windowing_sdk" name: "cover_display_opt_in" + is_exported: true description: "Properties to allow apps and activities to opt-in to cover display rendering" bug: "312530526" is_fixed_read_only: true diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java index 2e80b7e19516..c70febb3a7bf 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java @@ -20,7 +20,6 @@ import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHOR import static android.view.accessibility.AccessibilityManager.ShortcutType; import static com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode; -import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.createEnableDialogContentView; import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getInstalledTargets; import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets; import static com.android.internal.accessibility.util.AccessibilityUtils.isUserSetupCompleted; @@ -115,39 +114,22 @@ public class AccessibilityShortcutChooserActivity extends Activity { private void onTargetChecked(AdapterView<?> parent, View view, int position, long id) { final AccessibilityTarget target = mTargets.get(position); - if (Flags.cleanupAccessibilityWarningDialog()) { - if (target instanceof AccessibilityServiceTarget serviceTarget) { - if (sendRestrictedDialogIntentIfNeeded(target)) { - return; - } - final AccessibilityManager am = getSystemService(AccessibilityManager.class); - if (am.isAccessibilityServiceWarningRequired( - serviceTarget.getAccessibilityServiceInfo())) { - showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, - position, mTargetAdapter); - return; - } + if (target instanceof AccessibilityServiceTarget serviceTarget) { + if (sendRestrictedDialogIntentIfNeeded(target)) { + return; } - if (target instanceof AccessibilityActivityTarget activityTarget) { - if (!activityTarget.isShortcutEnabled() - && sendRestrictedDialogIntentIfNeeded(activityTarget)) { - return; - } + final AccessibilityManager am = getSystemService(AccessibilityManager.class); + if (am.isAccessibilityServiceWarningRequired( + serviceTarget.getAccessibilityServiceInfo())) { + showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, + position, mTargetAdapter); + return; } - } else { - if (!target.isShortcutEnabled()) { - if (target instanceof AccessibilityServiceTarget - || target instanceof AccessibilityActivityTarget) { - if (sendRestrictedDialogIntentIfNeeded(target)) { - return; - } - } - - if (target instanceof AccessibilityServiceTarget) { - showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, - position, mTargetAdapter); - return; - } + } + if (target instanceof AccessibilityActivityTarget activityTarget) { + if (!activityTarget.isShortcutEnabled() + && sendRestrictedDialogIntentIfNeeded(activityTarget)) { + return; } } @@ -178,37 +160,25 @@ public class AccessibilityShortcutChooserActivity extends Activity { return; } - if (Flags.cleanupAccessibilityWarningDialog()) { - mPermissionDialog = AccessibilityServiceWarning - .createAccessibilityServiceWarningDialog(context, - serviceTarget.getAccessibilityServiceInfo(), - v -> { - serviceTarget.onCheckedChanged(true); - targetAdapter.notifyDataSetChanged(); - mPermissionDialog.dismiss(); - }, v -> { - serviceTarget.onCheckedChanged(false); - mPermissionDialog.dismiss(); - }, - v -> { - mTargets.remove(position); - context.getPackageManager().getPackageInstaller().uninstall( - serviceTarget.getComponentName().getPackageName(), null); - targetAdapter.notifyDataSetChanged(); - mPermissionDialog.dismiss(); - }); - mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null); - } else { - mPermissionDialog = new AlertDialog.Builder(context) - .setView(createEnableDialogContentView(context, serviceTarget, - v -> { - mPermissionDialog.dismiss(); - targetAdapter.notifyDataSetChanged(); - }, - v -> mPermissionDialog.dismiss())) - .setOnDismissListener(dialog -> mPermissionDialog = null) - .create(); - } + mPermissionDialog = AccessibilityServiceWarning + .createAccessibilityServiceWarningDialog(context, + serviceTarget.getAccessibilityServiceInfo(), + v -> { + serviceTarget.onCheckedChanged(true); + targetAdapter.notifyDataSetChanged(); + mPermissionDialog.dismiss(); + }, v -> { + serviceTarget.onCheckedChanged(false); + mPermissionDialog.dismiss(); + }, + v -> { + mTargets.remove(position); + context.getPackageManager().getPackageInstaller().uninstall( + serviceTarget.getComponentName().getPackageName(), null); + targetAdapter.notifyDataSetChanged(); + mPermissionDialog.dismiss(); + }); + mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null); mPermissionDialog.show(); } diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java index 3d3db47faddb..0d82d63d8450 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java @@ -37,14 +37,8 @@ import android.content.Context; import android.os.Build; import android.os.UserHandle; import android.provider.Settings; -import android.text.BidiFormatter; -import android.view.LayoutInflater; -import android.view.View; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.ShortcutType; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; import com.android.internal.R; import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType; @@ -52,7 +46,6 @@ import com.android.internal.accessibility.common.ShortcutConstants.Accessibility import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Locale; /** * Collection of utilities for accessibility target. @@ -298,50 +291,6 @@ public final class AccessibilityTargetHelper { } /** - * @deprecated Use {@link AccessibilityServiceWarning}. - */ - @Deprecated - static View createEnableDialogContentView(Context context, - AccessibilityServiceTarget target, View.OnClickListener allowListener, - View.OnClickListener denyListener) { - final LayoutInflater inflater = (LayoutInflater) context.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - - final View content = inflater.inflate( - R.layout.accessibility_enable_service_warning, /* root= */ null); - - final ImageView dialogIcon = content.findViewById( - R.id.accessibility_permissionDialog_icon); - dialogIcon.setImageDrawable(target.getIcon()); - - final TextView dialogTitle = content.findViewById( - R.id.accessibility_permissionDialog_title); - dialogTitle.setText(context.getString(R.string.accessibility_enable_service_title, - getServiceName(context, target.getLabel()))); - - final Button allowButton = content.findViewById( - R.id.accessibility_permission_enable_allow_button); - final Button denyButton = content.findViewById( - R.id.accessibility_permission_enable_deny_button); - allowButton.setOnClickListener((view) -> { - target.onCheckedChanged(/* isChecked= */ true); - allowListener.onClick(view); - }); - denyButton.setOnClickListener((view) -> { - target.onCheckedChanged(/* isChecked= */ false); - denyListener.onClick(view); - }); - - return content; - } - - // Gets the service name and bidi wrap it to protect from bidi side effects. - private static CharSequence getServiceName(Context context, CharSequence label) { - final Locale locale = context.getResources().getConfiguration().getLocales().get(0); - return BidiFormatter.getInstance(locale).unicodeWrap(label); - } - - /** * Determines if the{@link AccessibilityTarget} is allowed. */ public static boolean isAccessibilityTargetAllowed(Context context, String packageName, diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 29669d312b1b..ab456a84d9ad 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -96,7 +96,6 @@ import android.provider.Downloads; import android.provider.OpenableColumns; import android.provider.Settings; import android.service.chooser.ChooserTarget; -import android.service.chooser.Flags; import android.text.TextUtils; import android.util.AttributeSet; import android.util.HashedStringCache; @@ -1801,54 +1800,6 @@ public class ChooserActivity extends ResolverActivity implements return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); } - private void showTargetDetails(TargetInfo targetInfo) { - if (targetInfo == null) return; - - ArrayList<DisplayResolveInfo> targetList; - ChooserTargetActionsDialogFragment fragment = new ChooserTargetActionsDialogFragment(); - Bundle bundle = new Bundle(); - - if (targetInfo instanceof SelectableTargetInfo) { - SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo; - if (selectableTargetInfo.getDisplayResolveInfo() == null - || selectableTargetInfo.getChooserTarget() == null) { - Log.e(TAG, "displayResolveInfo or chooserTarget in selectableTargetInfo are null"); - return; - } - targetList = new ArrayList<>(); - targetList.add(selectableTargetInfo.getDisplayResolveInfo()); - bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_ID_KEY, - selectableTargetInfo.getChooserTarget().getIntentExtras().getString( - Intent.EXTRA_SHORTCUT_ID)); - bundle.putBoolean(ChooserTargetActionsDialogFragment.IS_SHORTCUT_PINNED_KEY, - selectableTargetInfo.isPinned()); - bundle.putParcelable(ChooserTargetActionsDialogFragment.INTENT_FILTER_KEY, - getTargetIntentFilter()); - if (selectableTargetInfo.getDisplayLabel() != null) { - bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_TITLE_KEY, - selectableTargetInfo.getDisplayLabel().toString()); - } - } else if (targetInfo instanceof MultiDisplayResolveInfo) { - // For multiple targets, include info on all targets - MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo; - targetList = mti.getTargets(); - } else { - targetList = new ArrayList<DisplayResolveInfo>(); - targetList.add((DisplayResolveInfo) targetInfo); - } - // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be - // resolved correctly. - bundle.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY, - getResolveInfoUserHandle( - targetInfo.getResolveInfo(), - mChooserMultiProfilePagerAdapter.getCurrentUserHandle())); - bundle.putParcelableArrayList(ChooserTargetActionsDialogFragment.TARGET_INFOS_KEY, - targetList); - fragment.setArguments(bundle); - - fragment.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG); - } - private void modifyTargetIntent(Intent in) { if (isSendAction(in)) { in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | @@ -2544,10 +2495,7 @@ public class ChooserActivity extends ResolverActivity implements @Override public boolean isComponentPinned(ComponentName name) { - if (Flags.legacyChooserPinningRemoval()) { - return false; - } - return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false); + return false; } @Override @@ -3135,34 +3083,10 @@ public class ChooserActivity extends ResolverActivity implements if (isClickable) { itemView.setOnClickListener(v -> startSelected(mListPosition, false/* always */, true/* filterd */)); - - itemView.setOnLongClickListener(v -> { - final TargetInfo ti = mChooserMultiProfilePagerAdapter.getActiveListAdapter() - .targetInfoForPosition(mListPosition, /* filtered */ true); - - // This should always be the case for ItemViewHolder, check for validity - if (ti instanceof DisplayResolveInfo && shouldShowTargetDetails(ti)) { - showTargetDetails((DisplayResolveInfo) ti); - } - return true; - }); } } } - private boolean shouldShowTargetDetails(TargetInfo ti) { - if (Flags.legacyChooserPinningRemoval()) { - // Never show the long press menu if we've removed pinning. - return false; - } - ComponentName nearbyShare = getNearbySharingComponent(); - // Suppress target details for nearby share to hide pin/unpin action - boolean isNearbyShare = nearbyShare != null && nearbyShare.equals( - ti.getResolvedComponentName()) && shouldNearbyShareBeFirstInRankedRow(); - return ti instanceof SelectableTargetInfo - || (ti instanceof DisplayResolveInfo && !isNearbyShare); - } - /** * Add a footer to the list, to support scrolling behavior below the navbar. */ @@ -3517,16 +3441,6 @@ public class ChooserActivity extends ResolverActivity implements } }); - // Show menu for both direct share and app share targets after long click. - v.setOnLongClickListener(v1 -> { - TargetInfo ti = mChooserListAdapter.targetInfoForPosition( - holder.getItemIndex(column), true); - if (shouldShowTargetDetails(ti)) { - showTargetDetails(ti); - } - return true; - }); - holder.addView(i, v); // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll = diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 78f06b6bddb3..84715aa80edb 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -217,6 +217,12 @@ public class ResolverActivity extends Activity implements public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device"; /** + * Boolean extra to indicate if Resolver Sheet needs to be started in single user mode. + */ + protected static final String EXTRA_RESTRICT_TO_SINGLE_USER = + "com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER"; + + /** * Integer extra to indicate which profile should be automatically selected. * <p>Can only be used if there is a work profile. * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}. @@ -750,8 +756,10 @@ public class ResolverActivity extends Activity implements } protected UserHandle getPersonalProfileUserHandle() { - if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()){ - return mPrivateProfileUserHandle; + // When launched in single user mode, only personal tab is populated, so we use + // tabOwnerUserHandleForLaunch as personal tab's user handle. + if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) { + return getTabOwnerUserHandleForLaunch(); } return mPersonalProfileUserHandle; } @@ -822,11 +830,11 @@ public class ResolverActivity extends Activity implements // If we are in work or private profile's process, return WorkProfile/PrivateProfile user // as owner, otherwise we always return PersonalProfile user as owner if (UserHandle.of(UserHandle.myUserId()).equals(getWorkProfileUserHandle())) { - return getWorkProfileUserHandle(); + return mWorkProfileUserHandle; } else if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) { - return getPrivateProfileUserHandle(); + return mPrivateProfileUserHandle; } - return getPersonalProfileUserHandle(); + return mPersonalProfileUserHandle; } private boolean hasWorkProfile() { @@ -847,8 +855,18 @@ public class ResolverActivity extends Activity implements && (UserHandle.myUserId() == getPrivateProfileUserHandle().getIdentifier()); } + protected final boolean isLaunchedInSingleUserMode() { + // When launched from Private Profile, return true + if (isLaunchedAsPrivateProfile()) { + return true; + } + return getIntent() + .getBooleanExtra(EXTRA_RESTRICT_TO_SINGLE_USER, /* defaultValue = */ false); + } + protected boolean shouldShowTabs() { - if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) { + // No Tabs are shown when launched in single user mode. + if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) { return false; } return hasWorkProfile() && ENABLE_TABBED_VIEW; diff --git a/core/java/com/android/internal/app/SuspendedAppActivity.java b/core/java/com/android/internal/app/SuspendedAppActivity.java index 467cd49c2279..751368f8e041 100644 --- a/core/java/com/android/internal/app/SuspendedAppActivity.java +++ b/core/java/com/android/internal/app/SuspendedAppActivity.java @@ -16,6 +16,7 @@ package com.android.internal.app; +import static android.app.admin.flags.Flags.crossUserSuspensionEnabled; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.SuspendDialogInfo.BUTTON_ACTION_MORE_DETAILS; @@ -59,6 +60,7 @@ public class SuspendedAppActivity extends AlertActivity public static final String EXTRA_SUSPENDED_PACKAGE = PACKAGE_NAME + ".extra.SUSPENDED_PACKAGE"; public static final String EXTRA_SUSPENDING_PACKAGE = PACKAGE_NAME + ".extra.SUSPENDING_PACKAGE"; + public static final String EXTRA_SUSPENDING_USER = PACKAGE_NAME + ".extra.SUSPENDING_USER"; public static final String EXTRA_DIALOG_INFO = PACKAGE_NAME + ".extra.DIALOG_INFO"; public static final String EXTRA_ACTIVITY_OPTIONS = PACKAGE_NAME + ".extra.ACTIVITY_OPTIONS"; public static final String EXTRA_UNSUSPEND_INTENT = PACKAGE_NAME + ".extra.UNSUSPEND_INTENT"; @@ -67,6 +69,7 @@ public class SuspendedAppActivity extends AlertActivity private IntentSender mOnUnsuspend; private String mSuspendedPackage; private String mSuspendingPackage; + private int mSuspendingUserId; private int mNeutralButtonAction; private int mUserId; private PackageManager mPm; @@ -117,7 +120,7 @@ public class SuspendedAppActivity extends AlertActivity .setPackage(mSuspendingPackage); final String requiredPermission = Manifest.permission.SEND_SHOW_SUSPENDED_APP_DETAILS; final ResolveInfo resolvedInfo = mPm.resolveActivityAsUser(moreDetailsIntent, - MATCH_DIRECT_BOOT_UNAWARE | MATCH_DIRECT_BOOT_AWARE, mUserId); + MATCH_DIRECT_BOOT_UNAWARE | MATCH_DIRECT_BOOT_AWARE, mSuspendingUserId); if (resolvedInfo != null && resolvedInfo.activityInfo != null && requiredPermission.equals(resolvedInfo.activityInfo.permission)) { moreDetailsIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage) @@ -231,12 +234,17 @@ public class SuspendedAppActivity extends AlertActivity } mSuspendedPackage = intent.getStringExtra(EXTRA_SUSPENDED_PACKAGE); mSuspendingPackage = intent.getStringExtra(EXTRA_SUSPENDING_PACKAGE); + if (crossUserSuspensionEnabled()) { + mSuspendingUserId = intent.getIntExtra(EXTRA_SUSPENDING_USER, mUserId); + } else { + mSuspendingUserId = mUserId; + } mSuppliedDialogInfo = intent.getParcelableExtra(EXTRA_DIALOG_INFO, android.content.pm.SuspendDialogInfo.class); mOnUnsuspend = intent.getParcelableExtra(EXTRA_UNSUSPEND_INTENT, android.content.IntentSender.class); if (mSuppliedDialogInfo != null) { try { mSuspendingAppResources = createContextAsUser( - UserHandle.of(mUserId), /* flags */ 0).getPackageManager() + UserHandle.of(mSuspendingUserId), /* flags */ 0).getPackageManager() .getResourcesForApplication(mSuspendingPackage); } catch (PackageManager.NameNotFoundException ne) { Slog.e(TAG, "Could not find resources for " + mSuspendingPackage, ne); @@ -299,7 +307,7 @@ public class SuspendedAppActivity extends AlertActivity case BUTTON_ACTION_MORE_DETAILS: if (mMoreDetailsIntent != null) { startActivityAsUser(mMoreDetailsIntent, mOptions, - UserHandle.of(mUserId)); + UserHandle.of(mSuspendingUserId)); } else { Slog.wtf(TAG, "Neutral button should not have existed!"); } @@ -324,7 +332,7 @@ public class SuspendedAppActivity extends AlertActivity .putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage) .setPackage(mSuspendingPackage) .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); - sendBroadcastAsUser(reportUnsuspend, UserHandle.of(mUserId)); + sendBroadcastAsUser(reportUnsuspend, UserHandle.of(mSuspendingUserId)); if (mOnUnsuspend != null) { Bundle activityOptions = @@ -365,6 +373,9 @@ public class SuspendedAppActivity extends AlertActivity .putExtra(Intent.EXTRA_USER_ID, userId) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + if (crossUserSuspensionEnabled()) { + intent.putExtra(EXTRA_SUSPENDING_USER, suspendingPackage.userId); + } return intent; } } diff --git a/core/java/com/android/internal/compat/compat_logging_flags.aconfig b/core/java/com/android/internal/compat/compat_logging_flags.aconfig index fab3856daca7..a5c31edde473 100644 --- a/core/java/com/android/internal/compat/compat_logging_flags.aconfig +++ b/core/java/com/android/internal/compat/compat_logging_flags.aconfig @@ -2,7 +2,7 @@ package: "com.android.internal.compat.flags" flag { name: "skip_old_and_disabled_compat_logging" - namespace: "platform_compat" + namespace: "app_compat" description: "Feature flag for skipping debug logging for changes that do not target the latest sdk or are disabled" bug: "323949942" is_fixed_read_only: true diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 3662d69e1974..d2a533c78da6 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -124,10 +124,13 @@ public class Cuj { public static final int CUJ_BACK_PANEL_ARROW = 88; public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK = 89; public static final int CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH = 90; + public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = 91; + public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = 92; + public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = 93; // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE. @VisibleForTesting - static final int LAST_CUJ = CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH; + static final int LAST_CUJ = CUJ_LAUNCHER_SAVE_APP_PAIR; /** @hide */ @IntDef({ @@ -212,6 +215,9 @@ public class Cuj { CUJ_BACK_PANEL_ARROW, CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK, CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH, + CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE, + CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR, + CUJ_LAUNCHER_SAVE_APP_PAIR }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -306,6 +312,9 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_BACK_PANEL_ARROW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BACK_PANEL_ARROW; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_BACK; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_WEB_SEARCH; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SAVE_APP_PAIR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SAVE_APP_PAIR; } private Cuj() { @@ -484,6 +493,12 @@ public class Cuj { return "LAUNCHER_CLOSE_ALL_APPS_BACK"; case CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH: return "LAUNCHER_SEARCH_QSB_WEB_SEARCH"; + case CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE: + return "LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE"; + case CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR: + return "LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR"; + case CUJ_LAUNCHER_SAVE_APP_PAIR: + return "LAUNCHER_SAVE_APP_PAIR"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 0ec8b7461221..a288fb77749e 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -165,6 +165,9 @@ public class InteractionJankMonitor { @Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY = Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY; @Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_TASK = Cuj.CUJ_PREDICTIVE_BACK_CROSS_TASK; @Deprecated public static final int CUJ_PREDICTIVE_BACK_HOME = Cuj.CUJ_PREDICTIVE_BACK_HOME; + @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE; + @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR; + @Deprecated public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR; private static class InstanceHolder { public static final InteractionJankMonitor INSTANCE = diff --git a/core/java/com/android/internal/net/ConnectivityBlobStore.java b/core/java/com/android/internal/net/ConnectivityBlobStore.java new file mode 100644 index 000000000000..1b18485e35fa --- /dev/null +++ b/core/java/com/android/internal/net/ConnectivityBlobStore.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.net; + +import android.annotation.NonNull; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.os.Binder; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Database for storing blobs with a key of name strings. + * @hide + */ +public class ConnectivityBlobStore { + private static final String TAG = ConnectivityBlobStore.class.getSimpleName(); + private static final String TABLENAME = "blob_table"; + private static final String ROOT_DIR = "/data/misc/connectivityblobdb/"; + + private static final String CREATE_TABLE = + "CREATE TABLE IF NOT EXISTS " + TABLENAME + " (" + + "owner INTEGER," + + "name BLOB," + + "blob BLOB," + + "UNIQUE(owner, name));"; + + private final SQLiteDatabase mDb; + + /** + * Construct a ConnectivityBlobStore object. + * + * @param dbName the filename of the database to create/access. + */ + public ConnectivityBlobStore(String dbName) { + this(new File(ROOT_DIR + dbName)); + } + + @VisibleForTesting + public ConnectivityBlobStore(File file) { + final SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder() + .addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY) + .addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) + .build(); + mDb = SQLiteDatabase.openDatabase(file, params); + mDb.execSQL(CREATE_TABLE); + } + + /** + * Stores the blob under the name in the database. Existing blobs by the same name will be + * replaced. + * + * @param name The name of the blob + * @param blob The blob. + * @return true if the blob was successfully added. False otherwise. + * @hide + */ + public boolean put(@NonNull String name, @NonNull byte[] blob) { + final int ownerUid = Binder.getCallingUid(); + final ContentValues values = new ContentValues(); + values.put("owner", ownerUid); + values.put("name", name); + values.put("blob", blob); + + // No need for try-catch since it is done within db.replace + // nullColumnHack is for the case where values may be empty since SQL does not allow + // inserting a completely empty row. Since values is never empty, set this to null. + final long res = mDb.replace(TABLENAME, null /* nullColumnHack */, values); + return res > 0; + } + + /** + * Retrieves a blob by the name from the database. + * + * @param name Name of the blob to retrieve. + * @return The unstructured blob, that is the blob that was stored using + * {@link com.android.internal.net.ConnectivityBlobStore#put}. + * Returns null if no blob was found. + * @hide + */ + public byte[] get(@NonNull String name) { + final int ownerUid = Binder.getCallingUid(); + try (Cursor cursor = mDb.query(TABLENAME, + new String[] {"blob"} /* columns */, + "owner=? AND name=?" /* selection */, + new String[] {Integer.toString(ownerUid), name} /* selectionArgs */, + null /* groupBy */, + null /* having */, + null /* orderBy */)) { + if (cursor.moveToFirst()) { + return cursor.getBlob(0); + } + } catch (SQLException e) { + Log.e(TAG, "Error in getting " + name + ": " + e); + } + + return null; + } + + /** + * Removes a blob by the name from the database. + * + * @param name Name of the blob to be removed. + * @return True if a blob was removed. False if no such name was found. + * @hide + */ + public boolean remove(@NonNull String name) { + final int ownerUid = Binder.getCallingUid(); + try { + final int res = mDb.delete(TABLENAME, + "owner=? AND name=?" /* whereClause */, + new String[] {Integer.toString(ownerUid), name} /* whereArgs */); + return res > 0; + } catch (SQLException e) { + Log.e(TAG, "Error in removing " + name + ": " + e); + return false; + } + } + + /** + * Lists the name suffixes stored in the database matching the given prefix, sorted in + * ascending order. + * + * @param prefix String of prefix to list from the stored names. + * @return An array of strings representing the name suffixes stored in the database + * matching the given prefix, sorted in ascending order. + * The return value may be empty but never null. + * @hide + */ + public String[] list(@NonNull String prefix) { + final int ownerUid = Binder.getCallingUid(); + final List<String> names = new ArrayList<String>(); + try (Cursor cursor = mDb.query(TABLENAME, + new String[] {"name"} /* columns */, + "owner=? AND name LIKE ?" /* selection */, + new String[] {Integer.toString(ownerUid), prefix + "%"} /* selectionArgs */, + null /* groupBy */, + null /* having */, + "name ASC" /* orderBy */)) { + if (cursor.moveToFirst()) { + do { + final String name = cursor.getString(0); + names.add(name.substring(prefix.length())); + } while (cursor.moveToNext()); + } + } catch (SQLException e) { + Log.e(TAG, "Error in listing " + prefix + ": " + e); + } + + return names.toArray(new String[names.size()]); + } +} diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 0f1f7e9900c1..a65a1bb18303 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -137,7 +137,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind private static final int SCRIM_LIGHT = 0xe6ffffff; // 90% white - private static final int SCRIM_ALPHA = 0xcc0000; // 80% alpha + private static final int SCRIM_ALPHA = 0xcc000000; // 80% alpha public static final ColorViewAttributes STATUS_BAR_COLOR_VIEW_ATTRIBUTES = new ColorViewAttributes(FLAG_TRANSLUCENT_STATUS, diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index a22232ac945e..f5b1a47e917e 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -388,9 +388,9 @@ oneway interface IStatusBar */ void showMediaOutputSwitcher(String packageName); - /** Enters desktop mode. + /** Enters desktop mode from the current focused app. * * @param displayId the id of the current display. */ - void enterDesktop(int displayId); + void moveFocusedTaskToDesktop(int displayId); } diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index 600058e88e4b..e33704b0c535 100644 --- a/core/java/com/android/internal/view/BaseIWindow.java +++ b/core/java/com/android/internal/view/BaseIWindow.java @@ -33,6 +33,7 @@ import android.view.PointerIcon; import android.view.ScrollCaptureResponse; import android.view.WindowInsets.Type.InsetsType; import android.view.inputmethod.ImeTracker; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import com.android.internal.os.IResultReceiver; @@ -53,7 +54,8 @@ public class BaseIWindow extends IWindow.Stub { @Override public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, - boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing) { + boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing, + @Nullable ActivityWindowInfo activityWindowInfo) { if (reportDraw) { try { mSession.finishDrawing(this, null /* postDrawTransaction */, seqId); diff --git a/core/java/com/android/internal/widget/ConversationAvatarData.java b/core/java/com/android/internal/widget/ConversationAvatarData.java index e04772f72516..bc9cd40c110a 100644 --- a/core/java/com/android/internal/widget/ConversationAvatarData.java +++ b/core/java/com/android/internal/widget/ConversationAvatarData.java @@ -21,9 +21,9 @@ import android.graphics.drawable.Drawable; /** * @hide */ -interface ConversationAvatarData { +public interface ConversationAvatarData { final class OneToOneConversationAvatarData implements ConversationAvatarData { - final Drawable mDrawable; + public final Drawable mDrawable; OneToOneConversationAvatarData(Drawable drawable) { mDrawable = drawable; diff --git a/core/java/com/android/internal/widget/ConversationHeaderData.java b/core/java/com/android/internal/widget/ConversationHeaderData.java index 0953b3912a91..ea9215592c4b 100644 --- a/core/java/com/android/internal/widget/ConversationHeaderData.java +++ b/core/java/com/android/internal/widget/ConversationHeaderData.java @@ -21,7 +21,7 @@ import android.annotation.Nullable; /** * @hide */ -final class ConversationHeaderData { +public final class ConversationHeaderData { private final CharSequence mConversationText; private final ConversationAvatarData mConversationAvatarData; @@ -38,7 +38,7 @@ final class ConversationHeaderData { } @Nullable - ConversationAvatarData getConversationAvatar() { + public ConversationAvatarData getConversationAvatar() { return mConversationAvatarData; } } diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 6d5a96ae9a09..b6066ba5560f 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -162,6 +162,8 @@ public class ConversationLayout extends FrameLayout private TouchDelegateComposite mTouchDelegate = new TouchDelegateComposite(this); private ArrayList<MessagingLinearLayout.MessagingChild> mToRecycle = new ArrayList<>(); private boolean mPrecomputedTextEnabled = false; + @Nullable + private ConversationHeaderData mConversationHeaderData; public ConversationLayout(@NonNull Context context) { super(context); @@ -651,6 +653,7 @@ public class ConversationLayout extends FrameLayout private void setConversationAvatarAndNameFromData( ConversationHeaderData conversationHeaderData) { + mConversationHeaderData = conversationHeaderData; final OneToOneConversationAvatarData oneToOneConversationDrawable; final GroupConversationAvatarData groupConversationAvatarData; final ConversationAvatarData conversationAvatar = @@ -804,7 +807,10 @@ public class ConversationLayout extends FrameLayout bottomBackground.setLayoutParams(layoutParams); } - private void bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView, + /** + * Binds group avatar drawables to face pile. + */ + public void bindFacePileWithDrawable(ImageView bottomBackground, ImageView bottomView, ImageView topView, GroupConversationAvatarData groupConversationAvatarData) { applyNotificationBackgroundColor(bottomBackground); bottomView.setImageDrawable(groupConversationAvatarData.mLastIcon); @@ -1573,6 +1579,11 @@ public class ConversationLayout extends FrameLayout return mConversationIcon; } + @Nullable + public ConversationHeaderData getConversationHeaderData() { + return mConversationHeaderData; + } + private static class TouchDelegateComposite extends TouchDelegate { private final ArrayList<TouchDelegate> mDelegates = new ArrayList<>(); diff --git a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java index 3e065bf9f450..c07e62414ac2 100644 --- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java +++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java @@ -171,7 +171,9 @@ public class EmphasizedNotificationButton extends Button { return; } - prepareIcon(icon); + if (icon != null) { + prepareIcon(icon); + } mIconToGlue = icon; mGluePending = true; @@ -276,11 +278,6 @@ public class EmphasizedNotificationButton extends Button { // be ready to glue. This can only happen if the button is initialized and displayed and // *then* someone calls glueIcon or glueLabel. - if (mIconToGlue == null) { - Log.w(TAG, "glueIconAndLabelIfNeeded: label glued without icon; doing nothing"); - return; - } - if (mLabelToGlue == null) { Log.w(TAG, "glueIconAndLabelIfNeeded: icon glued without label; doing nothing"); return; @@ -316,6 +313,14 @@ public class EmphasizedNotificationButton extends Button { private static final String POP_DIRECTIONAL_ISOLATE = "\u2069"; private void glueIconAndLabel(int layoutDirection) { + if (mIconToGlue == null) { + if (DEBUG_NEW_ACTION_LAYOUT) { + Log.d(TAG, "glueIconAndLabel: null icon, setting text to label"); + } + setText(mLabelToGlue); + return; + } + final boolean rtlLayout = layoutDirection == LAYOUT_DIRECTION_RTL; if (DEBUG_NEW_ACTION_LAYOUT) { diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java index 5da64350619c..352e6d8e7b59 100644 --- a/core/java/com/android/internal/widget/ImageFloatingTextView.java +++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java @@ -31,6 +31,8 @@ import android.view.RemotableViewMethod; import android.widget.RemoteViews; import android.widget.TextView; +import com.android.internal.R; + /** * A TextView that can float around an image on the end. * @@ -49,6 +51,7 @@ public class ImageFloatingTextView extends TextView { private int mMaxLinesForHeight = -1; private int mLayoutMaxLines = -1; private int mImageEndMargin; + private final int mMaxLineUpperLimit; private int mStaticLayoutCreationCountInOnMeasure = 0; @@ -71,6 +74,8 @@ public class ImageFloatingTextView extends TextView { super(context, attrs, defStyleAttr, defStyleRes); setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL_FAST); setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY); + mMaxLineUpperLimit = + getResources().getInteger(R.integer.config_notificationLongTextMaxLineCount); } @Override @@ -102,6 +107,11 @@ public class ImageFloatingTextView extends TextView { } else { maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE; } + + if (mMaxLineUpperLimit > 0) { + maxLines = Math.min(maxLines, mMaxLineUpperLimit); + } + builder.setMaxLines(maxLines); mLayoutMaxLines = maxLines; if (shouldEllipsize) { diff --git a/core/jni/OWNERS b/core/jni/OWNERS index 3aca751edb0d..2a4f062478bd 100644 --- a/core/jni/OWNERS +++ b/core/jni/OWNERS @@ -27,6 +27,7 @@ per-file android_view_VelocityTracker.* = file:/services/core/java/com/android/s # WindowManager per-file android_graphics_BLASTBufferQueue.cpp = file:/services/core/java/com/android/server/wm/OWNERS per-file android_view_Surface* = file:/services/core/java/com/android/server/wm/OWNERS +per-file android_view_WindowManagerGlobal.cpp = file:/services/core/java/com/android/server/wm/OWNERS per-file android_window_* = file:/services/core/java/com/android/server/wm/OWNERS # Resources diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp index 3539476b8ce8..584ebaa221fc 100644 --- a/core/jni/android_os_Parcel.cpp +++ b/core/jni/android_os_Parcel.cpp @@ -661,6 +661,35 @@ static void android_os_Parcel_appendFrom(JNIEnv* env, jclass clazz, jlong thisNa return; } +static jboolean android_os_Parcel_hasBinders(JNIEnv* env, jclass clazz, jlong nativePtr) { + Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); + if (parcel != NULL) { + bool result; + status_t err = parcel->hasBinders(&result); + if (err != NO_ERROR) { + signalExceptionForError(env, clazz, err); + return JNI_FALSE; + } + return result ? JNI_TRUE : JNI_FALSE; + } + return JNI_FALSE; +} + +static jboolean android_os_Parcel_hasBindersInRange(JNIEnv* env, jclass clazz, jlong nativePtr, + jint offset, jint length) { + Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); + if (parcel != NULL) { + bool result; + status_t err = parcel->hasBindersInRange(offset, length, &result); + if (err != NO_ERROR) { + signalExceptionForError(env, clazz, err); + return JNI_FALSE; + } + return result ? JNI_TRUE : JNI_FALSE; + } + return JNI_FALSE; +} + static jboolean android_os_Parcel_hasFileDescriptors(jlong nativePtr) { jboolean ret = JNI_FALSE; @@ -806,7 +835,7 @@ static jboolean android_os_Parcel_replaceCallingWorkSourceUid(jlong nativePtr, j } // ---------------------------------------------------------------------------- - +// clang-format off static const JNINativeMethod gParcelMethods[] = { // @CriticalNative {"nativeMarkSensitive", "(J)V", (void*)android_os_Parcel_markSensitive}, @@ -886,6 +915,9 @@ static const JNINativeMethod gParcelMethods[] = { // @CriticalNative {"nativeHasFileDescriptors", "(J)Z", (void*)android_os_Parcel_hasFileDescriptors}, {"nativeHasFileDescriptorsInRange", "(JII)Z", (void*)android_os_Parcel_hasFileDescriptorsInRange}, + + {"nativeHasBinders", "(J)Z", (void*)android_os_Parcel_hasBinders}, + {"nativeHasBindersInRange", "(JII)Z", (void*)android_os_Parcel_hasBindersInRange}, {"nativeWriteInterfaceToken", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeInterfaceToken}, {"nativeEnforceInterface", "(JLjava/lang/String;)V", (void*)android_os_Parcel_enforceInterface}, @@ -900,6 +932,7 @@ static const JNINativeMethod gParcelMethods[] = { // @CriticalNative {"nativeReplaceCallingWorkSourceUid", "(JI)Z", (void*)android_os_Parcel_replaceCallingWorkSourceUid}, }; +// clang-format on const char* const kParcelPathName = "android/os/Parcel"; diff --git a/core/jni/android_os_Trace.cpp b/core/jni/android_os_Trace.cpp index 422bc1e8b59f..4387a4c63673 100644 --- a/core/jni/android_os_Trace.cpp +++ b/core/jni/android_os_Trace.cpp @@ -124,7 +124,7 @@ static void android_os_Trace_nativeInstantForTrack(JNIEnv* env, jclass, }); } -static jboolean android_os_Trace_nativeIsTagEnabled(JNIEnv* env, jlong tag) { +static jboolean android_os_Trace_nativeIsTagEnabled(jlong tag) { return tracing_perfetto::isTagEnabled(tag); } diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp index d2e58bb62c46..982189e30beb 100644 --- a/core/jni/android_util_Process.cpp +++ b/core/jni/android_util_Process.cpp @@ -1137,6 +1137,41 @@ void android_os_Process_sendSignalQuiet(JNIEnv* env, jobject clazz, jint pid, ji } } +void android_os_Process_sendSignalThrows(JNIEnv* env, jobject clazz, jint pid, jint sig) { + if (pid <= 0) { + jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", "Invalid argument: pid(%d)", + pid); + return; + } + int ret = kill(pid, sig); + if (ret < 0) { + if (errno == ESRCH) { + jniThrowExceptionFmt(env, "java/util/NoSuchElementException", + "Process with pid %d not found", pid); + } else { + signalExceptionForError(env, errno, pid); + } + } +} + +void android_os_Process_sendTgSignalThrows(JNIEnv* env, jobject clazz, jint tgid, jint tid, + jint sig) { + if (tgid <= 0 || tid <= 0) { + jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", + "Invalid argument: tgid(%d), tid(%d)", tid, tgid); + return; + } + int ret = tgkill(tgid, tid, sig); + if (ret < 0) { + if (errno == ESRCH) { + jniThrowExceptionFmt(env, "java/util/NoSuchElementException", + "Process with tid %d and tgid %d not found", tid, tgid); + } else { + signalExceptionForError(env, errno, tid); + } + } +} + static jlong android_os_Process_getElapsedCpuTime(JNIEnv* env, jobject clazz) { struct timespec ts; @@ -1357,6 +1392,8 @@ static const JNINativeMethod methods[] = { {"setGid", "(I)I", (void*)android_os_Process_setGid}, {"sendSignal", "(II)V", (void*)android_os_Process_sendSignal}, {"sendSignalQuiet", "(II)V", (void*)android_os_Process_sendSignalQuiet}, + {"sendSignalThrows", "(II)V", (void*)android_os_Process_sendSignalThrows}, + {"sendTgSignalThrows", "(III)V", (void*)android_os_Process_sendTgSignalThrows}, {"setProcessFrozen", "(IIZ)V", (void*)android_os_Process_setProcessFrozen}, {"getFreeMemory", "()J", (void*)android_os_Process_getFreeMemory}, {"getTotalMemory", "()J", (void*)android_os_Process_getTotalMemory}, diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index 1eab9910b651..1aa635c6ceb7 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -208,10 +208,17 @@ static struct { static struct { jclass clazz; jmethodID ctor; + jfieldID timeoutMillis; +} gIdleScreenRefreshRateConfigClassInfo; + +static struct { + jclass clazz; + jmethodID ctor; jfieldID defaultMode; jfieldID allowGroupSwitching; jfieldID primaryRanges; jfieldID appRequestRanges; + jfieldID idleScreenRefreshRateConfig; } gDesiredDisplayModeSpecsClassInfo; static struct { @@ -1407,6 +1414,18 @@ static jboolean nativeSetDesiredDisplayModeSpecs(JNIEnv* env, jclass clazz, jobj return ranges; }; + const auto makeIdleScreenRefreshRateConfig = [env](jobject obj) + -> std::optional<gui::DisplayModeSpecs::IdleScreenRefreshRateConfig> { + if (obj == NULL) { + return std::nullopt; + } + gui::DisplayModeSpecs::IdleScreenRefreshRateConfig idleScreenRefreshRateConfig; + idleScreenRefreshRateConfig.timeoutMillis = + env->GetIntField(obj, gIdleScreenRefreshRateConfigClassInfo.timeoutMillis); + + return idleScreenRefreshRateConfig; + }; + gui::DisplayModeSpecs specs; specs.defaultMode = env->GetIntField(DesiredDisplayModeSpecs, gDesiredDisplayModeSpecsClassInfo.defaultMode); @@ -1421,6 +1440,10 @@ static jboolean nativeSetDesiredDisplayModeSpecs(JNIEnv* env, jclass clazz, jobj makeRanges(env->GetObjectField(DesiredDisplayModeSpecs, gDesiredDisplayModeSpecsClassInfo.appRequestRanges)); + specs.idleScreenRefreshRateConfig = makeIdleScreenRefreshRateConfig( + env->GetObjectField(DesiredDisplayModeSpecs, + gDesiredDisplayModeSpecsClassInfo.idleScreenRefreshRateConfig)); + size_t result = SurfaceComposerClient::setDesiredDisplayModeSpecs(token, specs); return result == NO_ERROR ? JNI_TRUE : JNI_FALSE; } @@ -1440,6 +1463,17 @@ static jobject nativeGetDesiredDisplayModeSpecs(JNIEnv* env, jclass clazz, jobje rangeToJava(ranges.physical), rangeToJava(ranges.render)); }; + const auto idleScreenRefreshRateConfigToJava = + [env](const std::optional<gui::DisplayModeSpecs::IdleScreenRefreshRateConfig>& + idleScreenRefreshRateConfig) -> jobject { + if (!idleScreenRefreshRateConfig.has_value()) { + return NULL; // Return null if input config is null + } + return env->NewObject(gIdleScreenRefreshRateConfigClassInfo.clazz, + gIdleScreenRefreshRateConfigClassInfo.ctor, + idleScreenRefreshRateConfig->timeoutMillis); + }; + gui::DisplayModeSpecs specs; if (SurfaceComposerClient::getDesiredDisplayModeSpecs(token, &specs) != NO_ERROR) { return nullptr; @@ -1448,7 +1482,8 @@ static jobject nativeGetDesiredDisplayModeSpecs(JNIEnv* env, jclass clazz, jobje return env->NewObject(gDesiredDisplayModeSpecsClassInfo.clazz, gDesiredDisplayModeSpecsClassInfo.ctor, specs.defaultMode, specs.allowGroupSwitching, rangesToJava(specs.primaryRanges), - rangesToJava(specs.appRequestRanges)); + rangesToJava(specs.appRequestRanges), + idleScreenRefreshRateConfigToJava(specs.idleScreenRefreshRateConfig)); } static jobject nativeGetDisplayNativePrimaries(JNIEnv* env, jclass, jobject tokenObj) { @@ -2607,13 +2642,23 @@ int register_android_view_SurfaceControl(JNIEnv* env) GetFieldIDOrDie(env, RefreshRateRangesClazz, "render", "Landroid/view/SurfaceControl$RefreshRateRange;"); + jclass IdleScreenRefreshRateConfigClazz = + FindClassOrDie(env, "android/view/SurfaceControl$IdleScreenRefreshRateConfig"); + gIdleScreenRefreshRateConfigClassInfo.clazz = + MakeGlobalRefOrDie(env, IdleScreenRefreshRateConfigClazz); + gIdleScreenRefreshRateConfigClassInfo.ctor = + GetMethodIDOrDie(env, gIdleScreenRefreshRateConfigClassInfo.clazz, "<init>", "(I)V"); + gIdleScreenRefreshRateConfigClassInfo.timeoutMillis = + GetFieldIDOrDie(env, gIdleScreenRefreshRateConfigClassInfo.clazz, "timeoutMillis", "I"); + jclass DesiredDisplayModeSpecsClazz = FindClassOrDie(env, "android/view/SurfaceControl$DesiredDisplayModeSpecs"); gDesiredDisplayModeSpecsClassInfo.clazz = MakeGlobalRefOrDie(env, DesiredDisplayModeSpecsClazz); gDesiredDisplayModeSpecsClassInfo.ctor = GetMethodIDOrDie(env, gDesiredDisplayModeSpecsClassInfo.clazz, "<init>", "(IZLandroid/view/SurfaceControl$RefreshRateRanges;Landroid/view/" - "SurfaceControl$RefreshRateRanges;)V"); + "SurfaceControl$RefreshRateRanges;Landroid/view/" + "SurfaceControl$IdleScreenRefreshRateConfig;)V"); gDesiredDisplayModeSpecsClassInfo.defaultMode = GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "defaultMode", "I"); gDesiredDisplayModeSpecsClassInfo.allowGroupSwitching = @@ -2624,6 +2669,9 @@ int register_android_view_SurfaceControl(JNIEnv* env) gDesiredDisplayModeSpecsClassInfo.appRequestRanges = GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "appRequestRanges", "Landroid/view/SurfaceControl$RefreshRateRanges;"); + gDesiredDisplayModeSpecsClassInfo.idleScreenRefreshRateConfig = + GetFieldIDOrDie(env, DesiredDisplayModeSpecsClazz, "idleScreenRefreshRateConfig", + "Landroid/view/SurfaceControl$IdleScreenRefreshRateConfig;"); jclass jankDataClazz = FindClassOrDie(env, "android/view/SurfaceControl$JankData"); diff --git a/core/jni/android_view_WindowManagerGlobal.cpp b/core/jni/android_view_WindowManagerGlobal.cpp index b03ac88a36ca..abc621d8dc90 100644 --- a/core/jni/android_view_WindowManagerGlobal.cpp +++ b/core/jni/android_view_WindowManagerGlobal.cpp @@ -48,7 +48,7 @@ std::shared_ptr<InputChannel> createInputChannel( surfaceControlObj(env, android_view_SurfaceControl_getJavaSurfaceControl(env, surfaceControl)); - jobject clientTokenObj = javaObjectForIBinder(env, clientToken); + ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken)); ScopedLocalRef<jobject> clientInputTransferTokenObj( env, android_window_InputTransferToken_getJavaInputTransferToken(env, @@ -57,7 +57,7 @@ std::shared_ptr<InputChannel> createInputChannel( inputChannelObj(env, env->CallStaticObjectMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.createInputChannel, - clientTokenObj, + clientTokenObj.get(), hostInputTransferTokenObj.get(), surfaceControlObj.get(), clientInputTransferTokenObj.get())); @@ -68,9 +68,9 @@ std::shared_ptr<InputChannel> createInputChannel( void removeInputChannel(const sp<IBinder>& clientToken) { JNIEnv* env = AndroidRuntime::getJNIEnv(); - jobject clientTokenObj(javaObjectForIBinder(env, clientToken)); + ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken)); env->CallStaticObjectMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.removeInputChannel, - clientTokenObj); + clientTokenObj.get()); } int register_android_view_WindowManagerGlobal(JNIEnv* env) { diff --git a/core/jni/android_window_InputTransferToken.cpp b/core/jni/android_window_InputTransferToken.cpp index 8fb668d6bbd9..5bcea9b7c401 100644 --- a/core/jni/android_window_InputTransferToken.cpp +++ b/core/jni/android_window_InputTransferToken.cpp @@ -70,6 +70,11 @@ static jobject nativeGetBinderToken(JNIEnv* env, jclass clazz, jlong nativeObj) return javaObjectForIBinder(env, inputTransferToken->mToken); } +static jlong nativeGetBinderTokenRef(JNIEnv*, jclass, jlong nativeObj) { + sp<InputTransferToken> inputTransferToken = reinterpret_cast<InputTransferToken*>(nativeObj); + return reinterpret_cast<jlong>(inputTransferToken->mToken.get()); +} + InputTransferToken* android_window_InputTransferToken_getNativeInputTransferToken( JNIEnv* env, jobject inputTransferTokenObj) { if (inputTransferTokenObj != nullptr && @@ -114,6 +119,7 @@ static const JNINativeMethod sInputTransferTokenMethods[] = { {"nativeWriteToParcel", "(JLandroid/os/Parcel;)V", (void*)nativeWriteToParcel}, {"nativeReadFromParcel", "(Landroid/os/Parcel;)J", (void*)nativeReadFromParcel}, {"nativeGetBinderToken", "(J)Landroid/os/IBinder;", (void*)nativeGetBinderToken}, + {"nativeGetBinderTokenRef", "(J)J", (void*)nativeGetBinderTokenRef}, {"nativeGetNativeInputTransferTokenFinalizer", "()J", (void*)nativeGetNativeInputTransferTokenFinalizer}, {"nativeEquals", "(JJ)Z", (void*) nativeEquals}, // clang-format on diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 763d9ce1a053..fcc85b7ec90f 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -102,6 +102,7 @@ message SecureSettingsProto { optional SettingProto qs_targets = 54 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto accessibility_pinch_to_zoom_anywhere_enabled = 55 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto accessibility_single_finger_panning_enabled = 56 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto accessibility_floating_menu_targets = 57 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Accessibility accessibility = 2; @@ -143,9 +144,11 @@ message SecureSettingsProto { optional SettingProto gesture_setup_complete = 9 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto touch_gesture_enabled = 10 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto long_press_home_enabled = 11 [ (android.privacy).dest = DEST_AUTOMATIC ]; - optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC ]; - optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC ]; + // Deprecated - use search_all_entrypoints_enabled instead + optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC, deprecated = true ]; + optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC, deprecated = true ]; optional SettingProto visual_query_accessibility_detection_enabled = 14 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto search_all_entrypoints_enabled = 15 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Assist assist = 7; diff --git a/core/proto/android/service/package.proto b/core/proto/android/service/package.proto index 068f4dd07ccb..d30f195bf094 100644 --- a/core/proto/android/service/package.proto +++ b/core/proto/android/service/package.proto @@ -142,6 +142,7 @@ message PackageProto { // UTC timestamp of first install for the user optional int32 first_install_time_ms = 11; optional ArchiveState archive_state = 12; + repeated int32 suspending_user = 13; } message InstallSourceProto { diff --git a/core/res/res/drawable/activity_embedding_divider_handle.xml b/core/res/res/drawable/activity_embedding_divider_handle.xml new file mode 100644 index 000000000000..d9f363cb33a7 --- /dev/null +++ b/core/res/res/drawable/activity_embedding_divider_handle.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true" + android:drawable="@drawable/activity_embedding_divider_handle_pressed" /> + <item android:drawable="@drawable/activity_embedding_divider_handle_default" /> +</selector>
\ No newline at end of file diff --git a/core/res/res/drawable/activity_embedding_divider_handle_default.xml b/core/res/res/drawable/activity_embedding_divider_handle_default.xml new file mode 100644 index 000000000000..565f67169ab5 --- /dev/null +++ b/core/res/res/drawable/activity_embedding_divider_handle_default.xml @@ -0,0 +1,23 @@ +<!-- + 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="@dimen/activity_embedding_divider_handle_radius" /> + <size + android:width="@dimen/activity_embedding_divider_handle_width" + android:height="@dimen/activity_embedding_divider_handle_height" /> + <solid android:color="@color/activity_embedding_divider_color" /> +</shape>
\ No newline at end of file diff --git a/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml b/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml new file mode 100644 index 000000000000..e5cca2397806 --- /dev/null +++ b/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml @@ -0,0 +1,23 @@ +<!-- + 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="@dimen/activity_embedding_divider_handle_radius_pressed" /> + <size + android:width="@dimen/activity_embedding_divider_handle_width_pressed" + android:height="@dimen/activity_embedding_divider_handle_height_pressed" /> + <solid android:color="@color/activity_embedding_divider_color_pressed" /> +</shape>
\ No newline at end of file diff --git a/core/res/res/drawable/autofill_dataset_picker_background.xml b/core/res/res/drawable/autofill_dataset_picker_background.xml index d57497037616..6c4ef11f3879 100644 --- a/core/res/res/drawable/autofill_dataset_picker_background.xml +++ b/core/res/res/drawable/autofill_dataset_picker_background.xml @@ -16,7 +16,7 @@ <inset xmlns:android="http://schemas.android.com/apk/res/android"> <shape android:shape="rectangle"> - <corners android:radius="@dimen/config_bottomDialogCornerRadius" /> + <corners android:radius="@dimen/config_buttonCornerRadius" /> <solid android:color="?attr/colorBackground" /> </shape> </inset> diff --git a/core/res/res/layout/transient_notification_with_icon.xml b/core/res/res/layout/transient_notification_with_icon.xml index 0dfb3adc8364..04518b2a75a2 100644 --- a/core/res/res/layout/transient_notification_with_icon.xml +++ b/core/res/res/layout/transient_notification_with_icon.xml @@ -22,7 +22,7 @@ android:orientation="horizontal" android:gravity="center_vertical" android:maxWidth="@dimen/toast_width" - android:background="?android:attr/colorBackground" + android:background="@android:drawable/toast_frame" android:elevation="@dimen/toast_elevation" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" @@ -31,8 +31,11 @@ <ImageView android:id="@android:id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp" + android:layout_marginEnd="10dp" /> <TextView android:id="@android:id/message" diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index 417c6df1e30d..e6719195565e 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -593,6 +593,10 @@ <color name="accessibility_magnification_thumbnail_container_background_color">#99000000</color> <color name="accessibility_magnification_thumbnail_container_stroke_color">#FFFFFF</color> + <!-- Activity Embedding divider --> + <color name="activity_embedding_divider_color">#8e918f</color> + <color name="activity_embedding_divider_color_pressed">#e3e3e3</color> + <!-- Lily Language Picker language item view colors --> <color name="language_picker_item_text_color">#202124</color> <color name="language_picker_item_text_color_secondary">#5F6368</color> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index e3f1cb619eb5..1d6b151e2278 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -1483,6 +1483,11 @@ <!-- Number of notifications to keep in the notification service historical archive --> <integer name="config_notificationServiceArchiveSize">100</integer> + <!-- Upper limit imposed for long text content for BigTextStyle, MessagingStyle and + ConversationStyle notifications for performance reasons, and that line count is also + capped by vertical space available. It is only enabled when the value is positive int.--> + <integer name="config_notificationLongTextMaxLineCount">10</integer> + <!-- Allow the menu hard key to be disabled in LockScreen on some devices --> <bool name="config_disableMenuKeyInLockScreen">false</bool> @@ -6414,10 +6419,8 @@ <!-- Default value for Settings.ASSIST_TOUCH_GESTURE_ENABLED --> <bool name="config_assistTouchGestureEnabledDefault">true</bool> - <!-- Default value for Settings.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED --> - <bool name="config_searchPressHoldNavHandleEnabledDefault">true</bool> - <!-- Default value for Settings.ASSIST_LONG_PRESS_HOME_ENABLED for search overlay --> - <bool name="config_searchLongPressHomeEnabledDefault">true</bool> + <!-- Default value for Settings.SEARCH_ALL_ENTRYPOINTS_ENABLED --> + <bool name="config_searchAllEntrypointsEnabledDefault">true</bool> <!-- The maximum byte size of the information contained in the bundle of HotwordDetectedResult. --> @@ -6981,4 +6984,7 @@ <!-- Whether WM DisplayContent supports high performance transitions (lower-end devices may want to disable) --> <bool name="config_deviceSupportsHighPerfTransitions">true</bool> + + <!-- Wear devices: An intent action that is used for remote intent. --> + <string name="config_wearRemoteIntentAction" translatable="false" /> </resources> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 291a5936330a..4aa741de80a5 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -1028,6 +1028,16 @@ <dimen name="popup_enter_animation_from_y_delta">20dp</dimen> <dimen name="popup_exit_animation_to_y_delta">-10dp</dimen> + <!-- Dimensions for the activity embedding divider. --> + <dimen name="activity_embedding_divider_handle_width">4dp</dimen> + <dimen name="activity_embedding_divider_handle_height">48dp</dimen> + <dimen name="activity_embedding_divider_handle_radius">2dp</dimen> + <dimen name="activity_embedding_divider_handle_width_pressed">12dp</dimen> + <dimen name="activity_embedding_divider_handle_height_pressed">53dp</dimen> + <dimen name="activity_embedding_divider_handle_radius_pressed">6dp</dimen> + <dimen name="activity_embedding_divider_touch_target_width">24dp</dimen> + <dimen name="activity_embedding_divider_touch_target_height">64dp</dimen> + <!-- Default handwriting bounds offsets for editors. --> <dimen name="handwriting_bounds_offset_left">10dp</dimen> <dimen name="handwriting_bounds_offset_top">40dp</dimen> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index f915f038dc0d..a3dba48bbb7d 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -231,8 +231,10 @@ <string name="NetworkPreferenceSwitchSummary">Try changing preferred network. Tap to change.</string> <!-- Displayed to tell the user that emergency calls might not be available. --> <string name="EmergencyCallWarningTitle">Emergency calling unavailable</string> - <!-- Displayed to tell the user that emergency calls might not be available. --> - <string name="EmergencyCallWarningSummary">Can\u2019t make emergency calls over Wi\u2011Fi</string> + <!-- Displayed to tell the user that emergency calls might not be available; this is shown to + the user when only WiFi calling is available and the carrier does not support emergency + calls over WiFi calling. --> + <string name="EmergencyCallWarningSummary">Emergency calls require a mobile network</string> <!-- Telephony notification channel name for a channel containing network alert notifications. --> <string name="notification_channel_network_alert">Alerts</string> @@ -3247,6 +3249,12 @@ <!-- Title for EditText context menu [CHAR LIMIT=20] --> <string name="editTextMenuTitle">Text actions</string> + <!-- Error shown when a user uses a stylus to try handwriting on a text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] --> + <string name="error_handwriting_unsupported">Handwriting is not supported in this field</string> + + <!-- Error shown when a user uses a stylus to try handwriting on a password text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] --> + <string name="error_handwriting_unsupported_password">Handwriting is not supported in password fields</string> + <!-- Content description of the back button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="input_method_nav_back_button_desc">Back</string> <!-- Content description of the switch input method button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index f4b42f6b3fb2..4322b55b3f35 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2078,6 +2078,7 @@ <java-symbol type="integer" name="config_notificationsBatteryMediumARGB" /> <java-symbol type="integer" name="config_notificationsBatteryNearlyFullLevel" /> <java-symbol type="integer" name="config_notificationServiceArchiveSize" /> + <java-symbol type="integer" name="config_notificationLongTextMaxLineCount" /> <java-symbol type="dimen" name="config_rotaryEncoderAxisScrollTickInterval" /> <java-symbol type="integer" name="config_recentVibrationsDumpSizeLimit" /> <java-symbol type="integer" name="config_previousVibrationsDumpSizeLimit" /> @@ -3121,6 +3122,8 @@ <!-- TextView --> <java-symbol type="bool" name="config_textShareSupported" /> <java-symbol type="string" name="failed_to_copy_to_clipboard" /> + <java-symbol type="string" name="error_handwriting_unsupported" /> + <java-symbol type="string" name="error_handwriting_unsupported_password" /> <java-symbol type="id" name="notification_material_reply_container" /> <java-symbol type="id" name="notification_material_reply_text_1" /> @@ -5016,8 +5019,7 @@ <java-symbol type="bool" name="config_assistLongPressHomeEnabledDefault" /> <java-symbol type="bool" name="config_assistTouchGestureEnabledDefault" /> - <java-symbol type="bool" name="config_searchPressHoldNavHandleEnabledDefault" /> - <java-symbol type="bool" name="config_searchLongPressHomeEnabledDefault" /> + <java-symbol type="bool" name="config_searchAllEntrypointsEnabledDefault" /> <java-symbol type="integer" name="config_hotwordDetectedResultMaxBundleSize" /> @@ -5335,6 +5337,11 @@ <java-symbol type="raw" name="default_ringtone_vibration_effect" /> + <!-- For activity embedding divider --> + <java-symbol type="drawable" name="activity_embedding_divider_handle" /> + <java-symbol type="dimen" name="activity_embedding_divider_touch_target_width" /> + <java-symbol type="dimen" name="activity_embedding_divider_touch_target_height" /> + <!-- Whether we order unlocking and waking --> <java-symbol type="bool" name="config_orderUnlockAndWake" /> @@ -5378,4 +5385,6 @@ <!-- Whether WM DisplayContent supports high performance transitions --> <java-symbol type="bool" name="config_deviceSupportsHighPerfTransitions" /> + + <java-symbol type="string" name="config_wearRemoteIntentAction" /> </resources> diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml index 7d740ef76daf..c8625b9114da 100644 --- a/core/res/res/xml/sms_short_codes.xml +++ b/core/res/res/xml/sms_short_codes.xml @@ -42,8 +42,8 @@ <!-- Argentina: 5 digits, known short codes listed --> <shortcode country="ar" pattern="\\d{5}" free="11711|28291|44077|78887" /> - <!-- Armenia: 3-4 digits, emergency numbers 10[123] --> - <shortcode country="am" pattern="\\d{3,4}" premium="11[2456]1|3024" free="10[123]" /> + <!-- Armenia: 3-5 digits, emergency numbers 10[123] --> + <shortcode country="am" pattern="\\d{3,5}" premium="11[2456]1|3024" free="10[123]|71522|71512|71502" /> <!-- Austria: 10 digits, premium prefix 09xx, plus EU --> <shortcode country="at" pattern="11\\d{4}" premium="09.*" free="116\\d{3}" /> @@ -111,7 +111,7 @@ <shortcode country="do" pattern="\\d{1,6}" free="912892" /> <!-- Ecuador: 1-6 digits (standard system default, not country specific) --> - <shortcode country="ec" pattern="\\d{1,6}" free="466453" /> + <shortcode country="ec" pattern="\\d{1,6}" free="466453|18512" /> <!-- Estonia: short codes 3-5 digits starting with 1, plus premium 7 digit numbers starting with 90, plus EU. http://www.tja.ee/public/documents/Elektrooniline_side/Oigusaktid/ENG/Estonian_Numbering_Plan_annex_06_09_2010.mht --> @@ -137,11 +137,11 @@ visual voicemail code for EE: 887 --> <shortcode country="gb" pattern="\\d{4,6}" premium="[5-8]\\d{4}" free="116\\d{3}|2020|35890|61002|61202|887|83669|34664|40406|60174|7726|37726|88555|9017|9018" /> - <!-- Georgia: 4 digits, known premium codes listed --> - <shortcode country="ge" pattern="\\d{4}" premium="801[234]|888[239]" /> + <!-- Georgia: 1-5 digits, known premium codes listed --> + <shortcode country="ge" pattern="\\d{1,5}" premium="801[234]|888[239]" free="95201|95202|95203" /> <!-- Ghana: 4 digits, known premium codes listed --> - <shortcode country="gh" pattern="\\d{4}" free="5041" /> + <shortcode country="gh" pattern="\\d{4}" free="5041|3777" /> <!-- Greece: 5 digits (54xxx, 19yxx, x=0-9, y=0-5): http://www.cmtelecom.com/premium-sms/greece --> <shortcode country="gr" pattern="\\d{5}" premium="54\\d{3}|19[0-5]\\d{2}" free="116\\d{3}|12115" /> @@ -210,6 +210,9 @@ <!-- Macedonia: 1-6 digits (not confirmed), known premium codes listed --> <shortcode country="mk" pattern="\\d{1,6}" free="129005|122" /> + <!-- Mongolia : 1-6 digits (standard system default, not country specific) --> + <shortcode country="mn" pattern="\\d{1,6}" free="44444|45678|445566" /> + <!-- Malawi: 1-5 digits (standard system default, not country specific) --> <shortcode country="mw" pattern="\\d{1,5}" free="4276" /> @@ -247,7 +250,7 @@ <shortcode country="ph" pattern="\\d{1,5}" free="2147|5495|5496" /> <!-- Pakistan --> - <shortcode country="pk" pattern="\\d{1,5}" free="2057|9092" /> + <shortcode country="pk" pattern="\\d{1,6}" free="2057|9092|909203" /> <!-- Palestine: 5 digits, known premium codes listed --> <shortcode country="ps" pattern="\\d{1,5}" free="37477|6681" /> @@ -291,7 +294,7 @@ <shortcode country="sk" premium="\\d{4}" free="116\\d{3}|8000" /> <!-- Senegal(SN): 1-5 digits (standard system default, not country specific) --> - <shortcode country="sn" pattern="\\d{1,5}" free="21215" /> + <shortcode country="sn" pattern="\\d{1,5}" free="21215|21098" /> <!-- El Salvador(SV): 1-5 digits (standard system default, not country specific) --> <shortcode country="sv" pattern="\\d{4,6}" free="466453" /> @@ -321,14 +324,17 @@ visual voicemail code for T-Mobile: 122 --> <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" standard="44567|244444" free="122|87902|21696|24614|28003|30356|33669|40196|41064|41270|43753|44034|46645|52413|56139|57969|61785|66975|75136|76227|81398|83952|85140|86566|86799|95737|96684|99245|611611|96831" /> + <!--Uruguay : 1-5 digits (standard system default, not country specific) --> + <shortcode country="uy" pattern="\\d{1,5}" free="55002" /> + <!-- Vietnam: 1-5 digits (standard system default, not country specific) --> - <shortcode country="vn" pattern="\\d{1,5}" free="5001|9055" /> + <shortcode country="vn" pattern="\\d{1,5}" free="5001|9055|8079" /> <!-- Mayotte (French Territory): 1-5 digits (not confirmed) --> <shortcode country="yt" pattern="\\d{1,5}" free="38600,36300,36303,959" /> <!-- South Africa --> - <shortcode country="za" pattern="\\d{1,5}" free="44136|30791|36056" /> + <shortcode country="za" pattern="\\d{1,5}" free="44136|30791|36056|33009" /> <!-- Zimbabwe --> <shortcode country="zw" pattern="\\d{1,5}" free="33679" /> diff --git a/core/tests/bugreports/OWNERS b/core/tests/bugreports/OWNERS new file mode 100644 index 000000000000..dbd767c78853 --- /dev/null +++ b/core/tests/bugreports/OWNERS @@ -0,0 +1,2 @@ +# Bug component: 153446 +file:/platform/frameworks/native:/cmds/dumpstate/OWNERS diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java index 990739745f24..2ce7a7d3d70d 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java @@ -88,6 +88,7 @@ public class ClientTransactionItemTest { private InsetsState mInsetsState; private ClientWindowFrames mFrames; private MergedConfiguration mMergedConfiguration; + private ActivityWindowInfo mActivityWindowInfo; @Before public void setup() { @@ -99,6 +100,7 @@ public class ClientTransactionItemTest { mInsetsState = new InsetsState(); mFrames = new ClientWindowFrames(); mMergedConfiguration = new MergedConfiguration(mGlobalConfig, mConfiguration); + mActivityWindowInfo = new ActivityWindowInfo(); doReturn(mActivity).when(mHandler).getActivity(mActivityToken); doReturn(mActivitiesToBeDestroyed).when(mHandler).getActivitiesToBeDestroyed(); @@ -107,7 +109,7 @@ public class ClientTransactionItemTest { @Test public void testActivityConfigurationChangeItem_getContextToUpdate() { final ActivityConfigurationChangeItem item = ActivityConfigurationChangeItem - .obtain(mActivityToken, mConfiguration, new ActivityWindowInfo()); + .obtain(mActivityToken, mConfiguration, mActivityWindowInfo); final Context context = item.getContextToUpdate(mHandler); assertEquals(mActivity, context); @@ -118,7 +120,7 @@ public class ClientTransactionItemTest { final ActivityRelaunchItem item = ActivityRelaunchItem .obtain(mActivityToken, null /* pendingResults */, null /* pendingNewIntents */, 0 /* configChange */, mMergedConfiguration, false /* preserveWindow */, - new ActivityWindowInfo()); + mActivityWindowInfo); final Context context = item.getContextToUpdate(mHandler); assertEquals(mActivity, context); @@ -177,7 +179,7 @@ public class ClientTransactionItemTest { @Test public void testMoveToDisplayItem_getContextToUpdate() { final MoveToDisplayItem item = MoveToDisplayItem - .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration, new ActivityWindowInfo()); + .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration, mActivityWindowInfo); final Context context = item.getContextToUpdate(mHandler); assertEquals(mActivity, context); @@ -218,13 +220,13 @@ public class ClientTransactionItemTest { final WindowStateResizeItem item = WindowStateResizeItem.obtain(mWindow, mFrames, true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */, true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */, - true /* dragResizing */); + true /* dragResizing */, mActivityToken, mActivityWindowInfo); item.execute(mHandler, mPendingActions); verify(mWindow).resized(mFrames, true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */, true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */, - true /* dragResizing */); + true /* dragResizing */, mActivityWindowInfo); } @Test @@ -232,7 +234,7 @@ public class ClientTransactionItemTest { final WindowStateResizeItem item = WindowStateResizeItem.obtain(mWindow, mFrames, true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */, true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */, - true /* dragResizing */); + true /* dragResizing */, mActivityToken, mActivityWindowInfo); final Context context = item.getContextToUpdate(mHandler); assertEquals(ActivityThread.currentApplication(), context); diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java index 93c2e0e40593..40e79ad8ada3 100644 --- a/core/tests/coretests/src/android/os/BundleTest.java +++ b/core/tests/coretests/src/android/os/BundleTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.annotations.Presubmit; import android.platform.test.ravenwood.RavenwoodRule; @@ -445,6 +446,42 @@ public class BundleTest { assertThat(bundle.size()).isEqualTo(0); } + @Test + @DisabledOnRavenwood(blockedBy = Parcel.class) + public void parcelledBundleWithBinder_shouldReturnHasBindersTrue() throws Exception { + Bundle bundle = new Bundle(); + bundle.putParcelable("test", new CustomParcelable(13, "Tiramisu")); + bundle.putBinder("test_binder", + new IBinderWorkSourceNestedService.Stub() { + + public int[] nestedCallWithWorkSourceToSet(int uidToBlame) { + return new int[0]; + } + + public int[] nestedCall() { + return new int[0]; + } + }); + Bundle bundle2 = new Bundle(getParcelledBundle(bundle)); + assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_PRESENT); + + bundle2.putParcelable("test2", new CustomParcelable(13, "Tiramisu")); + assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_UNKNOWN); + } + + @Test + @DisabledOnRavenwood(blockedBy = Parcel.class) + public void parcelledBundleWithoutBinder_shouldReturnHasBindersFalse() throws Exception { + Bundle bundle = new Bundle(); + bundle.putParcelable("test", new CustomParcelable(13, "Tiramisu")); + Bundle bundle2 = new Bundle(getParcelledBundle(bundle)); + //Should fail to load with framework classloader. + assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_NOT_PRESENT); + + bundle2.putParcelable("test2", new CustomParcelable(13, "Tiramisu")); + assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_UNKNOWN); + } + private Bundle getMalformedBundle() { Parcel p = Parcel.obtain(); p.writeInt(BaseBundle.BUNDLE_MAGIC); @@ -520,6 +557,7 @@ public class BundleTest { public CustomParcelable createFromParcel(Parcel in) { return new CustomParcelable(in); } + @Override public CustomParcelable[] newArray(int size) { return new CustomParcelable[size]; diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java index 26f6d696768a..442394e3428a 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -347,4 +347,30 @@ public class ParcelTest { p.recycle(); Binder.setIsDirectlyHandlingTransactionOverride(false); } + + @Test + @IgnoreUnderRavenwood(blockedBy = Parcel.class) + public void testHasBinders_AfterWritingBinderToParcel() { + Binder binder = new Binder(); + Parcel pA = Parcel.obtain(); + int iA = pA.dataPosition(); + pA.writeInt(13); + assertFalse(pA.hasBinders()); + pA.writeStrongBinder(binder); + assertTrue(pA.hasBinders()); + } + + + @Test + @IgnoreUnderRavenwood(blockedBy = Parcel.class) + public void testHasBindersInRange_AfterWritingBinderToParcel() { + Binder binder = new Binder(); + Parcel pA = Parcel.obtain(); + pA.writeInt(13); + + int binderStartPos = pA.dataPosition(); + pA.writeStrongBinder(binder); + int binderEndPos = pA.dataPosition(); + assertTrue(pA.hasBinders(binderStartPos, binderEndPos - binderStartPos)); + } } diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index 316e191eecbd..97f894f8dcac 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -21,12 +21,11 @@ import static android.view.InsetsController.ANIMATION_TYPE_HIDE; import static android.view.InsetsController.ANIMATION_TYPE_NONE; import static android.view.InsetsController.ANIMATION_TYPE_RESIZE; import static android.view.InsetsController.ANIMATION_TYPE_SHOW; -import static android.view.InsetsController.AnimationType; +import static android.view.InsetsSource.FLAG_ANIMATE_RESIZING; import static android.view.InsetsSource.ID_IME; import static android.view.InsetsSourceConsumer.ShowResult.IME_SHOW_DELAYED; import static android.view.InsetsSourceConsumer.ShowResult.SHOW_IMMEDIATELY; import static android.view.ViewRootImpl.CAPTION_ON_SHELL; -import static android.view.WindowInsets.Type.SIZE; import static android.view.WindowInsets.Type.all; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.defaultVisible; @@ -671,36 +670,81 @@ public class InsetsControllerTest { } @Test - public void testResizeAnimation_insetsTypes() { - for (int i = 0; i < SIZE; i++) { - final @InsetsType int type = 1 << i; - final @AnimationType int expectedAnimationType = (type & systemBars()) != 0 - ? ANIMATION_TYPE_RESIZE - : ANIMATION_TYPE_NONE; - doTestResizeAnimation_insetsTypes(type, expectedAnimationType); - } + public void testResizeAnimation_withFlagAnimateResizing() { + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + final int id = ID_NAVIGATION_BAR; + final @InsetsType int type = navigationBars(); + final InsetsState state1 = new InsetsState(); + state1.getOrCreateSource(id, type) + .setVisible(true) + .setFrame(0, 0, 500, 50) + .setFlags(FLAG_ANIMATE_RESIZING, FLAG_ANIMATE_RESIZING); + final InsetsState state2 = new InsetsState(state1, true /* copySources */); + state2.peekSource(id).setFrame(0, 0, 500, 60); + + // New insets source won't cause the resize animation. + mController.onStateChanged(state1); + assertEquals("There must not be resize animation.", ANIMATION_TYPE_NONE, + mController.getAnimationType(type)); + + // Changing frame of the source with FLAG_ANIMATE_RESIZING will cause the resize + // animation. + mController.onStateChanged(state2); + assertEquals("There must be resize animation.", ANIMATION_TYPE_RESIZE, + mController.getAnimationType(type)); + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); } - private void doTestResizeAnimation_insetsTypes(@InsetsType int type, - @AnimationType int expectedAnimationType) { - final int id = type; + @Test + public void testResizeAnimation_withoutFlagAnimateResizing() { InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + final int id = ID_STATUS_BAR; + final @InsetsType int type = statusBars(); final InsetsState state1 = new InsetsState(); - state1.getOrCreateSource(id, type).setVisible(true).setFrame(0, 0, 500, 50); + state1.getOrCreateSource(id, type) + .setVisible(true) + .setFrame(0, 0, 500, 50) + .setFlags(0, FLAG_ANIMATE_RESIZING); final InsetsState state2 = new InsetsState(state1, true /* copySources */); state2.peekSource(id).setFrame(0, 0, 500, 60); - final String message = "Animation type of " + WindowInsets.Type.toString(type) + ":"; + final String message = "There must not be resize animation."; + + // New insets source won't cause the resize animation. + mController.onStateChanged(state1); + assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type)); + + // Changing frame of the source without FLAG_ANIMATE_RESIZING must not cause the resize + // animation. + mController.onStateChanged(state2); + assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type)); + }); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + @Test + public void testResizeAnimation_sourceFrame() { + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + final int id = ID_STATUS_BAR; + final @InsetsType int type = statusBars(); + final InsetsState state1 = new InsetsState(); + state1.setDisplayFrame(new Rect(0, 0, 500, 1000)); + state1.getOrCreateSource(id, type).setFrame(0, 0, 500, 50); + final InsetsState state2 = new InsetsState(state1, true /* copySources */); + state2.setDisplayFrame(state1.getDisplayFrame()); + state2.peekSource(id).setFrame(0, 0, 500, 0); + final String message = "There must not be resize animation."; // New insets source won't cause the resize animation. mController.onStateChanged(state1); assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type)); - // Changing frame might cause the resize animation. This depends on the insets type. + // Changing frame won't cause the resize animation if the new frame is empty. mController.onStateChanged(state2); - assertEquals(message, expectedAnimationType, mController.getAnimationType(type)); + assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type)); - // Cancel the existing animations for the next iteration. - mController.cancelExistingAnimations(); + // Changing frame won't cause the resize animation if the existing frame is empty. + mController.onStateChanged(state1); assertEquals(message, ANIMATION_TYPE_NONE, mController.getAnimationType(type)); }); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java index 90a8c5c57fc2..226629e2019e 100644 --- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java +++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java @@ -16,7 +16,14 @@ package android.view; +import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; +import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; +import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; +import static android.view.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_DEFAULT_NORMAL_READ_ONLY; +import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; +import static android.view.flags.Flags.toolkitFrameRateBySizeReadOnly; +import static android.view.flags.Flags.toolkitFrameRateDefaultNormalReadOnly; import static junit.framework.Assert.assertEquals; @@ -124,6 +131,7 @@ public class ViewFrameRateTest { } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void noVelocityUsesCategorySmall() throws Throwable { final CountDownLatch drawLatch1 = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { @@ -141,12 +149,14 @@ public class ViewFrameRateTest { // Now that it is small, any invalidation should have a normal category mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); - assertEquals(Surface.FRAME_RATE_CATEGORY_NORMAL, - mViewRoot.getPreferredFrameRateCategory()); + int expected = toolkitFrameRateBySizeReadOnly() + ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL; + assertEquals(expected, mViewRoot.getPreferredFrameRateCategory()); }); } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void noVelocityUsesCategoryNarrowWidth() throws Throwable { final CountDownLatch drawLatch1 = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { @@ -164,12 +174,14 @@ public class ViewFrameRateTest { // Now that it is small, any invalidation should have a normal category mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); - assertEquals(Surface.FRAME_RATE_CATEGORY_NORMAL, - mViewRoot.getPreferredFrameRateCategory()); + int expected = toolkitFrameRateBySizeReadOnly() + ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL; + assertEquals(expected, mViewRoot.getPreferredFrameRateCategory()); }); } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void noVelocityUsesCategoryNarrowHeight() throws Throwable { final CountDownLatch drawLatch1 = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { @@ -187,12 +199,14 @@ public class ViewFrameRateTest { // Now that it is small, any invalidation should have a normal category mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); - assertEquals(Surface.FRAME_RATE_CATEGORY_NORMAL, - mViewRoot.getPreferredFrameRateCategory()); + int expected = toolkitFrameRateBySizeReadOnly() + ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL; + assertEquals(expected, mViewRoot.getPreferredFrameRateCategory()); }); } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void noVelocityUsesCategoryLargeWidth() throws Throwable { final CountDownLatch drawLatch1 = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { @@ -210,12 +224,14 @@ public class ViewFrameRateTest { // Now that it is small, any invalidation should have a high category mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); - assertEquals(Surface.FRAME_RATE_CATEGORY_HIGH, - mViewRoot.getPreferredFrameRateCategory()); + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, mViewRoot.getPreferredFrameRateCategory()); }); } @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void noVelocityUsesCategoryLargeHeight() throws Throwable { final CountDownLatch drawLatch1 = new CountDownLatch(1); mActivityRule.runOnUiThread(() -> { @@ -233,7 +249,20 @@ public class ViewFrameRateTest { // Now that it is small, any invalidation should have a high category mActivityRule.runOnUiThread(() -> { mMovingView.invalidate(); - assertEquals(Surface.FRAME_RATE_CATEGORY_HIGH, + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, mViewRoot.getPreferredFrameRateCategory()); + }); + } + + @Test + @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, + FLAG_TOOLKIT_FRAME_RATE_DEFAULT_NORMAL_READ_ONLY}) + public void defaultNormal() throws Throwable { + waitForFrameRateCategoryToSettle(); + mActivityRule.runOnUiThread(() -> { + mMovingView.invalidate(); + assertEquals(FRAME_RATE_CATEGORY_NORMAL, mViewRoot.getPreferredFrameRateCategory()); }); } diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 652011ba74cd..fa364e06a705 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -41,6 +41,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; +import static android.view.flags.Flags.toolkitFrameRateDefaultNormalReadOnly; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -81,6 +82,7 @@ import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -462,6 +464,7 @@ public class ViewRootImplTest { */ @UiThreadTest @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_getDefaultValues() { ViewRootImpl viewRootImpl = new ViewRootImpl(sContext, @@ -478,6 +481,7 @@ public class ViewRootImplTest { * Also, mIsFrameRateBoosting should be true when the visibility becomes visible */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY}) public void votePreferredFrameRate_voteFrameRateCategory_visibility_bySize() { @@ -511,6 +515,7 @@ public class ViewRootImplTest { * <7%: FRAME_RATE_CATEGORY_LOW */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY}) public void votePreferredFrameRate_voteFrameRateCategory_smallSize_bySize() { @@ -539,6 +544,7 @@ public class ViewRootImplTest { * >=7% : FRAME_RATE_CATEGORY_NORMAL */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, FLAG_TOOLKIT_FRAME_RATE_BY_SIZE_READ_ONLY}) public void votePreferredFrameRate_voteFrameRateCategory_normalSize_bySize() { @@ -571,6 +577,7 @@ public class ViewRootImplTest { * Also, mIsFrameRateBoosting should be true when the visibility becomes visible */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_voteFrameRateCategory_visibility_defaultHigh() { View view = new View(sContext); @@ -587,8 +594,9 @@ public class ViewRootImplTest { sInstrumentation.runOnMainSync(() -> { view.setVisibility(View.VISIBLE); view.invalidate(); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), - FRAME_RATE_CATEGORY_HIGH); + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, viewRootImpl.getPreferredFrameRateCategory()); }); sInstrumentation.waitForIdleSync(); @@ -603,6 +611,7 @@ public class ViewRootImplTest { * <7%: FRAME_RATE_CATEGORY_NORMAL */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_voteFrameRateCategory_smallSize_defaultHigh() { View view = new View(sContext); @@ -630,6 +639,7 @@ public class ViewRootImplTest { * >=7% : FRAME_RATE_CATEGORY_HIGH */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_voteFrameRateCategory_normalSize_defaultHigh() { View view = new View(sContext); @@ -650,7 +660,9 @@ public class ViewRootImplTest { ViewRootImpl viewRootImpl = view.getViewRootImpl(); sInstrumentation.runOnMainSync(() -> { view.invalidate(); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, viewRootImpl.getPreferredFrameRateCategory()); }); } @@ -659,6 +671,7 @@ public class ViewRootImplTest { * It should take the max value among all of the voted categories per frame. */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_voteFrameRateCategory_aggregate() { View view = new View(sContext); @@ -704,6 +717,7 @@ public class ViewRootImplTest { * prioritize 60Hz.. */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_voteFrameRate_aggregate() { View view = new View(sContext); @@ -762,6 +776,7 @@ public class ViewRootImplTest { * submit your preferred choice to the ViewRootImpl. */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_voteFrameRate_category() { View view = new View(sContext); @@ -801,6 +816,7 @@ public class ViewRootImplTest { * Also, we shouldn't call setFrameRate. */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, FLAG_VIEW_VELOCITY_API}) public void votePreferredFrameRate_voteFrameRateCategory_velocityToHigh() { View view = new View(sContext); @@ -832,6 +848,7 @@ public class ViewRootImplTest { * We should boost the frame rate if the value of mInsetsAnimationRunning is true. */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_insetsAnimation() { View view = new View(sContext); @@ -868,6 +885,7 @@ public class ViewRootImplTest { * Test FrameRateBoostOnTouchEnabled API */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_frameRateBoostOnTouch() { View view = new View(sContext); @@ -900,6 +918,7 @@ public class ViewRootImplTest { * mPreferredFrameRate should be set to 0. */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_voteFrameRateTimeOut() throws InterruptedException { final long delay = 200L; @@ -937,6 +956,7 @@ public class ViewRootImplTest { * A View should either vote a frame rate or a frame rate category instead of both. */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_voteFrameRateOnly() { View view = new View(sContext); @@ -979,6 +999,7 @@ public class ViewRootImplTest { * - otherwise, use the previous category value. */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_infrequentLayer_defaultHigh() throws InterruptedException { final long delay = 200L; @@ -1000,11 +1021,13 @@ public class ViewRootImplTest { ViewRootImpl viewRootImpl = view.getViewRootImpl(); - // In transistion from frequent update to infrequent update + // In transition from frequent update to infrequent update Thread.sleep(delay); sInstrumentation.runOnMainSync(() -> { view.invalidate(); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, viewRootImpl.getPreferredFrameRateCategory()); }); // reset the frame rate category counts @@ -1016,7 +1039,7 @@ public class ViewRootImplTest { sInstrumentation.waitForIdleSync(); } - // In transistion from frequent update to infrequent update + // In transition from frequent update to infrequent update Thread.sleep(delay); sInstrumentation.runOnMainSync(() -> { view.setRequestedFrameRate(view.REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE); @@ -1024,6 +1047,13 @@ public class ViewRootImplTest { assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NO_PREFERENCE); }); + Thread.sleep(delay); + sInstrumentation.runOnMainSync(() -> { + view.setRequestedFrameRate(view.REQUESTED_FRAME_RATE_CATEGORY_DEFAULT); + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_NO_PREFERENCE); + }); // Infrequent update Thread.sleep(delay); @@ -1039,6 +1069,7 @@ public class ViewRootImplTest { */ @UiThreadTest @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_isFrameRatePowerSavingsBalanced() { ViewRootImpl viewRootImpl = new ViewRootImpl(sContext, @@ -1056,6 +1087,7 @@ public class ViewRootImplTest { * 2. If FT2-FT1 > 15ms && FT3-FT2 > 15ms -> vote for NORMAL category */ @Test + @Ignore("Can be enabled only after b/330596920 is ready") @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void votePreferredFrameRate_applyTextureViewHeuristic() throws InterruptedException { final long delay = 30L; @@ -1081,8 +1113,9 @@ public class ViewRootImplTest { assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NO_PREFERENCE); view.invalidate(); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), - FRAME_RATE_CATEGORY_HIGH); + int expected = toolkitFrameRateDefaultNormalReadOnly() + ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH; + assertEquals(expected, viewRootImpl.getPreferredFrameRateCategory()); }); // reset the frame rate category counts diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index a5c962412024..faad472d4ad6 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -52,9 +52,11 @@ import android.view.PointerIcon; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.inputmethod.Flags; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -72,6 +74,7 @@ import org.mockito.ArgumentCaptor; */ @Presubmit @SmallTest +@UiThreadTest @RunWith(AndroidJUnit4.class) public class HandwritingInitiatorTest { private static final long TIMEOUT = ViewConfiguration.getLongPressTimeout(); @@ -133,7 +136,7 @@ public class HandwritingInitiatorTest { when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + onEditorFocusedOrConnectionCreated(mTestView1); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -170,7 +173,7 @@ public class HandwritingInitiatorTest { when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(2); - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + onEditorFocusedOrConnectionCreated(mTestView1); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -200,7 +203,7 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_startHandwritingOnce_when_stylusMoveMultiTimes_withinHWArea() { - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + onEditorFocusedOrConnectionCreated(mTestView1); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -244,9 +247,7 @@ public class HandwritingInitiatorTest { when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4); when(mTestView1.getLineAtCoordinate(anyFloat())).thenReturn(0); - if (!mInitiateWithoutConnection) { - mHandwritingInitiator.onInputConnectionCreated(mTestView1); - } + onEditorFocusedOrConnectionCreated(mTestView1); final int x1 = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2; final int y1 = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -282,13 +283,7 @@ public class HandwritingInitiatorTest { MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); mHandwritingInitiator.onTouchEvent(stylusEvent2); - if (mInitiateWithoutConnection) { - // Focus is changed after stylus movement. - mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true); - } else { - // InputConnection is created after stylus movement. - mHandwritingInitiator.onInputConnectionCreated(mTestView1); - } + onEditorFocusedOrConnectionCreated(mTestView1); verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1); } @@ -310,24 +305,11 @@ public class HandwritingInitiatorTest { final int y2 = y1; MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); mHandwritingInitiator.onTouchEvent(stylusEvent2); - - if (!mInitiateWithoutConnection) { - // First create InputConnection for mTestView2 and verify that handwriting is not - // started. - mHandwritingInitiator.onInputConnectionCreated(mTestView2); - } - + onEditorFocusedOrConnectionCreated(mTestView2); // Note: mTestView2 receives focus when initiationWithoutInputConnection() is enabled. // verify that handwriting is not started. verify(mHandwritingInitiator, never()).startHandwriting(mTestView2); - if (mInitiateWithoutConnection) { - // Focus is changed after stylus movement. - mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true); - } else { - // Next create InputConnection for mTextView1. Handwriting is started for this view - // since the stylus down point is closest to this view. - mHandwritingInitiator.onInputConnectionCreated(mTestView1); - } + onEditorFocusedOrConnectionCreated(mTestView1); // Handwriting is started for this view since the stylus down point is closest to this // view. verify(mHandwritingInitiator).startHandwriting(mTestView1); @@ -349,7 +331,7 @@ public class HandwritingInitiatorTest { delegateView.setIsHandwritingDelegate(true); mTestView1.setHandwritingDelegatorCallback( - () -> mHandwritingInitiator.onInputConnectionCreated(delegateView)); + () -> onEditorFocusedOrConnectionCreated(delegateView)); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; @@ -369,17 +351,15 @@ public class HandwritingInitiatorTest { public void onTouchEvent_tryAcceptDelegation_delegatorCallbackFocusesDelegate() { View delegateView = new EditText(mContext); delegateView.setIsHandwritingDelegate(true); + if (mInitiateWithoutConnection) { + mHandwritingInitiator.onEditorFocused(delegateView); + } mHandwritingInitiator.onInputConnectionCreated(delegateView); reset(mHandwritingInitiator); - if (mInitiateWithoutConnection) { - mTestView1.setHandwritingDelegatorCallback( - () -> mHandwritingInitiator.updateFocusedView( - delegateView, /*fromTouchEvent*/ false)); - } else { - mTestView1.setHandwritingDelegatorCallback( - () -> mHandwritingInitiator.onDelegateViewFocused(delegateView)); - } + + mTestView1.setHandwritingDelegatorCallback( + () -> mHandwritingInitiator.onDelegateViewFocused(delegateView)); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; @@ -391,7 +371,7 @@ public class HandwritingInitiatorTest { MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0); mHandwritingInitiator.onTouchEvent(stylusEvent2); - verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(delegateView); + verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(any()); } @Test @@ -429,14 +409,6 @@ public class HandwritingInitiatorTest { assertThat(onTouchEventResult4).isTrue(); } - private void callOnInputConnectionOrUpdateViewFocus(View view) { - if (mInitiateWithoutConnection) { - mHandwritingInitiator.updateFocusedView(view, /*fromTouchEvent*/ true); - } else { - mHandwritingInitiator.onInputConnectionCreated(view); - } - } - @Test public void onTouchEvent_notStartHandwriting_whenHandwritingNotAvailable() { final Rect rect = new Rect(600, 600, 900, 900); @@ -444,7 +416,7 @@ public class HandwritingInitiatorTest { false /* isStylusHandwritingAvailable */); mHandwritingInitiator.updateHandwritingAreasForView(testView); - callOnInputConnectionOrUpdateViewFocus(testView); + onEditorFocusedOrConnectionCreated(testView); final int x1 = (rect.left + rect.right) / 2; final int y1 = (rect.top + rect.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -463,7 +435,7 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_notStartHandwriting_when_stylusTap_withinHWArea() { - callOnInputConnectionOrUpdateViewFocus(mTestView1); + onEditorFocusedOrConnectionCreated(mTestView1); final int x1 = 200; final int y1 = 200; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -479,7 +451,7 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_notStartHandwriting_when_stylusMove_outOfHWArea() { - callOnInputConnectionOrUpdateViewFocus(mTestView1); + onEditorFocusedOrConnectionCreated(mTestView1); final int x1 = 10; final int y1 = 10; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -495,7 +467,7 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_notStartHandwriting_when_stylusMove_afterTimeOut() { - callOnInputConnectionOrUpdateViewFocus(mTestView1); + onEditorFocusedOrConnectionCreated(mTestView1); final int x1 = 10; final int y1 = 10; final long time1 = 10L; @@ -551,9 +523,7 @@ public class HandwritingInitiatorTest { @Test public void onTouchEvent_focusView_inputConnectionAlreadyBuilt_stylusMoveOnce_withinHWArea() { - if (!mInitiateWithoutConnection) { - mHandwritingInitiator.onInputConnectionCreated(mTestView1); - } + onEditorFocusedOrConnectionCreated(mTestView1); final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0); @@ -606,14 +576,14 @@ public class HandwritingInitiatorTest { verify(mTestView2, times(1)).requestFocus(); - callOnInputConnectionOrUpdateViewFocus(mTestView2); + onEditorFocusedOrConnectionCreated(mTestView2); verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView2); } @Test public void onTouchEvent_handwritingAreaOverlapped_focusedViewHasPriority() { // Simulate the case where mTestView1 is focused. - callOnInputConnectionOrUpdateViewFocus(mTestView1); + onEditorFocusedOrConnectionCreated(mTestView1); // The ACTION_DOWN location is within the handwriting bounds of both mTestView1 and // mTestView2. Although it's closer to mTestView2's handwriting bounds, handwriting is // initiated for mTestView1 because it's focused. @@ -651,7 +621,7 @@ public class HandwritingInitiatorTest { @Test public void onResolvePointerIcon_afterHandwriting_hidePointerIconForConnectedView() { // simulate the case where sTestView1 is focused. - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + onEditorFocusedOrConnectionCreated(mTestView1); injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(), /* exceedsHWSlop */ true); // Verify that handwriting started for sTestView1. @@ -677,15 +647,14 @@ public class HandwritingInitiatorTest { public void onResolvePointerIcon_afterHandwriting_hidePointerIconForDelegatorView() { // Set mTextView2 to be the delegate of mTestView1. mTestView2.setIsHandwritingDelegate(true); + mTestView1.setHandwritingDelegatorCallback( + () -> { + if (mInitiateWithoutConnection) { + mHandwritingInitiator.updateFocusedView(mTestView2); + } + mHandwritingInitiator.onInputConnectionCreated(mTestView2); + }); - if (mInitiateWithoutConnection) { - mTestView1.setHandwritingDelegatorCallback( - () -> mHandwritingInitiator.updateFocusedView( - mTestView2, /*fromTouchEvent*/ false)); - } else { - mTestView1.setHandwritingDelegatorCallback( - () -> mHandwritingInitiator.onInputConnectionCreated(mTestView2)); - } injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(), /* exceedsHWSlop */ true); // Prerequisite check, verify that handwriting started for delegateView. @@ -700,7 +669,7 @@ public class HandwritingInitiatorTest { @Test public void onResolvePointerIcon_showHoverIconAfterTap() { // Simulate the case where sTestView1 is focused. - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + onEditorFocusedOrConnectionCreated(mTestView1); injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(), /* exceedsHWSlop */ true); // Verify that handwriting started for sTestView1. @@ -722,7 +691,7 @@ public class HandwritingInitiatorTest { @Test public void onResolvePointerIcon_showHoverIconAfterFocusChange() { // Simulate the case where sTestView1 is focused. - mHandwritingInitiator.onInputConnectionCreated(mTestView1); + onEditorFocusedOrConnectionCreated(mTestView1); injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(), /* exceedsHWSlop */ true); // Verify that handwriting started for sTestView1. @@ -733,14 +702,8 @@ public class HandwritingInitiatorTest { // After handwriting is initiated for the connected view, hide the hover icon. assertThat(icon1).isNull(); - // Simulate that focus is switched to mTestView2 first and then switched back. - if (mInitiateWithoutConnection) { - mHandwritingInitiator.updateFocusedView(mTestView2, /*fromTouchEvent*/ true); - mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true); - } else { - mHandwritingInitiator.onInputConnectionCreated(mTestView2); - mHandwritingInitiator.onInputConnectionCreated(mTestView1); - } + onEditorFocusedOrConnectionCreated(mTestView2); + onEditorFocusedOrConnectionCreated(mTestView1); PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1); // After the change of focus, hover icon shows again. @@ -752,11 +715,11 @@ public class HandwritingInitiatorTest { if (mInitiateWithoutConnection) { mTestView1.setAutoHandwritingEnabled(false); mTestView1.setHandwritingDelegatorCallback(null); - mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true); + onEditorFocusedOrConnectionCreated(mTestView1); } else { View mockView = createView(sHwArea1, false /* autoHandwritingEnabled */, true /* isStylusHandwritingAvailable */); - mHandwritingInitiator.onInputConnectionCreated(mockView); + onEditorFocusedOrConnectionCreated(mockView); } final int x1 = (sHwArea1.left + sHwArea1.right) / 2; final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2; @@ -972,4 +935,12 @@ public class HandwritingInitiatorTest { 1 /* yPrecision */, 0 /* deviceId */, 0 /* edgeFlags */, InputDevice.SOURCE_STYLUS, 0 /* flags */); } + + private void onEditorFocusedOrConnectionCreated(View testView) { + if (Flags.initiationWithoutInputConnection()) { + mHandwritingInitiator.onEditorFocused(testView); + } else { + mHandwritingInitiator.onInputConnectionCreated(testView); + } + } } diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java index 60a436e6b2c2..745390d1648e 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java @@ -25,7 +25,6 @@ import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.RootMatchers.isDialog; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withClassName; -import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static com.google.common.truth.Truth.assertThat; @@ -54,7 +53,6 @@ import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.os.Bundle; import android.os.Handler; -import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -176,21 +174,6 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) - public void selectTestService_oldPermissionDialog_deny_dialogIsHidden() { - launchActivity(); - openShortcutsList(); - - mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS); - onView(withText(DENY_LABEL)).perform(scrollTo(), click()); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - - onView(withId(R.id.accessibility_permissionDialog_title)).inRoot(isDialog()).check( - doesNotExist()); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_allow_rowChecked() { launchActivity(); openShortcutsList(); @@ -202,7 +185,6 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_deny_rowNotChecked() { launchActivity(); openShortcutsList(); @@ -214,7 +196,6 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_uninstall_callsUninstaller_rowRemoved() { launchActivity(); openShortcutsList(); @@ -228,7 +209,6 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_notShownWhenNotRequired() throws Exception { when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any())) .thenReturn(false); @@ -243,7 +223,6 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_notPermittedByAdmin_blockedEvenIfNoWarningRequired() throws Exception { when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any())) @@ -380,11 +359,9 @@ public class AccessibilityShortcutChooserActivityTest { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (Flags.cleanupAccessibilityWarningDialog()) { - // Setting the Theme is necessary here for the dialog to use the proper style - // resources as designated in its layout XML. - setTheme(R.style.Theme_DeviceDefault_DayNight); - } + // Setting the Theme is necessary here for the dialog to use the proper style + // resources as designated in its layout XML. + setTheme(R.style.Theme_DeviceDefault_DayNight); } @Override diff --git a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java index 24aab6192c50..362eeeacfc1e 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java @@ -25,7 +25,6 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.app.AlertDialog; import android.content.Context; import android.os.RemoteException; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; @@ -57,8 +56,6 @@ import java.util.concurrent.atomic.AtomicBoolean; */ @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -@RequiresFlagsEnabled( - android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public class AccessibilityServiceWarningTest { private static final String A11Y_SERVICE_PACKAGE_LABEL = "TestA11yService"; private static final String A11Y_SERVICE_SUMMARY = "TestA11yService summary"; diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java index cb8754ae9962..488f017872b1 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java @@ -27,6 +27,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static com.android.internal.app.MatcherUtils.first; +import static com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER; import static com.android.internal.app.ResolverDataProvider.createPackageManagerMockedInfo; import static com.android.internal.app.ResolverWrapperActivity.sOverrides; @@ -1254,6 +1255,51 @@ public class ResolverActivityTest { } } + @Test + public void testTriggerFromMainProfile_inSingleUserMode_withWorkProfilePresent() { + mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, + android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE); + markWorkProfileUserAvailable(); + setTabOwnerUserHandleForLaunch(PERSONAL_USER_HANDLE); + Intent sendIntent = createSendImageIntent(); + sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + assertThat(activity.getPersonalListAdapter().getCount(), is(2)); + onView(withId(R.id.tabs)).check(matches(not(isDisplayed()))); + assertEquals(activity.getMultiProfilePagerAdapterCount(), 1); + for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) { + assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle, PERSONAL_USER_HANDLE); + } + } + + @Test + public void testTriggerFromWorkProfile_inSingleUserMode() { + mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, + android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE); + markWorkProfileUserAvailable(); + setTabOwnerUserHandleForLaunch(sOverrides.workProfileUserHandle); + Intent sendIntent = createSendImageIntent(); + sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle); + setupResolverControllers(personalResolvedComponentInfos); + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + assertThat(activity.getPersonalListAdapter().getCount(), is(3)); + onView(withId(R.id.tabs)).check(matches(not(isDisplayed()))); + assertEquals(activity.getMultiProfilePagerAdapterCount(), 1); + for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) { + assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle, + sOverrides.workProfileUserHandle); + } + } + private Intent createSendImageIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); @@ -1339,6 +1385,10 @@ public class ResolverActivityTest { ResolverWrapperActivity.sOverrides.privateProfileUserHandle = UserHandle.of(12); } + private void setTabOwnerUserHandleForLaunch(UserHandle tabOwnerUserHandleForLaunch) { + sOverrides.tabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch; + } + private void setupResolverControllers( List<ResolvedComponentInfo> personalResolvedComponentInfos, List<ResolvedComponentInfo> workResolvedComponentInfos) { diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java index 862cbd5b5e01..4604b01d1bd2 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java @@ -116,6 +116,10 @@ public class ResolverWrapperActivity extends ResolverActivity { when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM); return sOverrides.resolverListController; } + if (isLaunchedInSingleUserMode()) { + when(sOverrides.resolverListController.getUserHandle()).thenReturn(userHandle); + return sOverrides.resolverListController; + } when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); return sOverrides.workResolverListController; } diff --git a/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java b/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java new file mode 100644 index 000000000000..68545cfe889c --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.net; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ConnectivityBlobStoreTest { + private static final String DATABASE_FILENAME = "ConnectivityBlobStore.db"; + private static final String TEST_NAME = "TEST_NAME"; + private static final byte[] TEST_BLOB = new byte[] {(byte) 10, (byte) 90, (byte) 45, (byte) 12}; + + private Context mContext; + private File mFile; + + private ConnectivityBlobStore createConnectivityBlobStore() { + return new ConnectivityBlobStore(mFile); + } + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getContext(); + mFile = mContext.getDatabasePath(DATABASE_FILENAME); + } + + @After + public void tearDown() throws Exception { + mContext.deleteDatabase(DATABASE_FILENAME); + } + + @Test + public void testFileCreateDelete() { + assertFalse(mFile.exists()); + createConnectivityBlobStore(); + assertTrue(mFile.exists()); + + assertTrue(mContext.deleteDatabase(DATABASE_FILENAME)); + assertFalse(mFile.exists()); + } + + @Test + public void testPutAndGet() throws Exception { + final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore(); + assertNull(connectivityBlobStore.get(TEST_NAME)); + + assertTrue(connectivityBlobStore.put(TEST_NAME, TEST_BLOB)); + assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(TEST_NAME)); + + // Test replacement + final byte[] newBlob = new byte[] {(byte) 15, (byte) 20}; + assertTrue(connectivityBlobStore.put(TEST_NAME, newBlob)); + assertArrayEquals(newBlob, connectivityBlobStore.get(TEST_NAME)); + } + + @Test + public void testRemove() throws Exception { + final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore(); + assertNull(connectivityBlobStore.get(TEST_NAME)); + assertFalse(connectivityBlobStore.remove(TEST_NAME)); + + assertTrue(connectivityBlobStore.put(TEST_NAME, TEST_BLOB)); + assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(TEST_NAME)); + + assertTrue(connectivityBlobStore.remove(TEST_NAME)); + assertNull(connectivityBlobStore.get(TEST_NAME)); + + // Removing again returns false + assertFalse(connectivityBlobStore.remove(TEST_NAME)); + } + + @Test + public void testMultipleNames() throws Exception { + final String name1 = TEST_NAME + "1"; + final String name2 = TEST_NAME + "2"; + final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore(); + + assertNull(connectivityBlobStore.get(name1)); + assertNull(connectivityBlobStore.get(name2)); + assertFalse(connectivityBlobStore.remove(name1)); + assertFalse(connectivityBlobStore.remove(name2)); + + assertTrue(connectivityBlobStore.put(name1, TEST_BLOB)); + assertTrue(connectivityBlobStore.put(name2, TEST_BLOB)); + assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(name1)); + assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(name2)); + + // Replace the blob for name1 only. + final byte[] newBlob = new byte[] {(byte) 16, (byte) 21}; + assertTrue(connectivityBlobStore.put(name1, newBlob)); + assertArrayEquals(newBlob, connectivityBlobStore.get(name1)); + + assertTrue(connectivityBlobStore.remove(name1)); + assertNull(connectivityBlobStore.get(name1)); + assertArrayEquals(TEST_BLOB, connectivityBlobStore.get(name2)); + + assertFalse(connectivityBlobStore.remove(name1)); + assertTrue(connectivityBlobStore.remove(name2)); + assertNull(connectivityBlobStore.get(name2)); + assertFalse(connectivityBlobStore.remove(name2)); + } + + @Test + public void testList() throws Exception { + final String[] unsortedNames = new String[] { + TEST_NAME + "1", + TEST_NAME + "2", + TEST_NAME + "0", + "NON_MATCHING_PREFIX", + "MATCHING_SUFFIX_" + TEST_NAME + }; + // Expected to match and discard the prefix and be in increasing sorted order. + final String[] expected = new String[] { + "0", + "1", + "2" + }; + final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore(); + + for (int i = 0; i < unsortedNames.length; i++) { + assertTrue(connectivityBlobStore.put(unsortedNames[i], TEST_BLOB)); + } + final String[] actual = connectivityBlobStore.list(TEST_NAME /* prefix */); + assertArrayEquals(expected, actual); + } +} diff --git a/core/tests/coretests/src/com/android/internal/net/OWNERS b/core/tests/coretests/src/com/android/internal/net/OWNERS new file mode 100644 index 000000000000..f51ba475ab63 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/net/OWNERS @@ -0,0 +1 @@ +include /core/java/com/android/internal/net/OWNERS diff --git a/data/etc/com.android.settings.xml b/data/etc/com.android.settings.xml index fbe1b8e65171..6bdd2914e831 100644 --- a/data/etc/com.android.settings.xml +++ b/data/etc/com.android.settings.xml @@ -49,6 +49,7 @@ <permission name="android.permission.READ_SEARCH_INDEXABLES"/> <permission name="android.permission.REBOOT"/> <permission name="android.permission.RECOVERY"/> + <permission name="android.permission.SCHEDULE_EXACT_ALARM"/> <permission name="android.permission.STATUS_BAR"/> <permission name="android.permission.SUGGEST_MANUAL_TIME_AND_ZONE"/> <permission name="android.permission.TETHER_PRIVILEGED"/> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 9c1c700641f1..ea3235bfff6c 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -588,6 +588,8 @@ applications that come with the platform <permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" /> <!-- Permission required for CTS test - PackageManagerShellCommandInstallTest --> <permission name="android.permission.EMERGENCY_INSTALL_PACKAGES" /> + <!-- Permission required for Cts test - CtsSettingsTestCases --> + <permission name="android.permission.PREPARE_FACTORY_RESET" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/data/keyboards/Vendor_054c_Product_05c4.idc b/data/keyboards/Vendor_054c_Product_05c4.idc index 2cb3f7b90fed..2da622745baf 100644 --- a/data/keyboards/Vendor_054c_Product_05c4.idc +++ b/data/keyboards/Vendor_054c_Product_05c4.idc @@ -13,9 +13,11 @@ # limitations under the License. # -# Sony DS4 motion sensor configuration file. +# Sony Playstation(R) DualShock 4 Controller # +## Motion sensor ## + # reporting mode 0 - continuous sensor.accelerometer.reportingMode = 0 # The delay between sensor events corresponding to the lowest frequency in microsecond @@ -33,3 +35,28 @@ sensor.gyroscope.maxDelay = 100000 sensor.gyroscope.minDelay = 5000 # The power in mA used by this sensor while in use sensor.gyroscope.power = 0.8 + +## Touchpad ## + +# After the DualShock 4 has been connected over Bluetooth for a minute or so, +# its reports start bunching up in time, meaning that we receive 2–4 reports +# within a millisecond followed by a >10ms wait until the next batch. +# +# This uneven timing causes the apparent speed of a finger (calculated using +# time deltas between received reports) to vary dramatically even if it's +# actually moving smoothly across the touchpad, triggering the touchpad stack's +# drumroll detection logic. For moving fingers, the drumroll detection logic +# splits the finger's single movement into many small movements of consecutive +# touches, which are then inhibited by the click wiggle filter. For tapping +# fingers, it prevents tapping to click because it thinks the finger's moving +# too fast. +# +# Since this touchpad doesn't seem to have to drumroll issues, we can safely +# disable drumroll detection. +gestureProp.Drumroll_Suppression_Enable = 0 + +# Because of the way this touchpad is positioned, touches around the edges are +# no more likely to be palms than ones in the middle, so remove the edge zones +# from the palm classifier to increase the usable area of the pad. +gestureProp.Palm_Edge_Zone_Width = 0 +gestureProp.Tap_Exclusion_Border_Width = 0 diff --git a/data/keyboards/Vendor_054c_Product_09cc.idc b/data/keyboards/Vendor_054c_Product_09cc.idc index 2cb3f7b90fed..2a1a4fc62b24 100644 --- a/data/keyboards/Vendor_054c_Product_09cc.idc +++ b/data/keyboards/Vendor_054c_Product_09cc.idc @@ -13,9 +13,11 @@ # limitations under the License. # -# Sony DS4 motion sensor configuration file. +# Sony Playstation(R) DualShock 4 Controller # +## Motion sensor ## + # reporting mode 0 - continuous sensor.accelerometer.reportingMode = 0 # The delay between sensor events corresponding to the lowest frequency in microsecond @@ -33,3 +35,28 @@ sensor.gyroscope.maxDelay = 100000 sensor.gyroscope.minDelay = 5000 # The power in mA used by this sensor while in use sensor.gyroscope.power = 0.8 + +## Touchpad ## + +# After the DualShock 4 has been connected over Bluetooth for a minute or so, +# its reports start bunching up in time, meaning that we receive 2–4 reports +# within a millisecond followed by a >10ms wait until the next batch. +# +# This uneven timing causes the apparent speed of a finger (calculated using +# time deltas between received reports) to vary dramatically even if it's +# actually moving smoothly across the touchpad, triggering the touchpad stack's +# drumroll detection logic. For moving fingers, the drumroll detection logic +# splits the finger's single movement into many small movements of consecutive +# touches, which are then inhibited by the click wiggle filter. For tapping +# fingers, it prevents tapping to click because it thinks the finger's moving +# too fast. +# +# Since this touchpad doesn't seem to have drumroll issues, we can safely +# disable drumroll detection. +gestureProp.Drumroll_Suppression_Enable = 0 + +# Because of the way this touchpad is positioned, touches around the edges are +# no more likely to be palms than ones in the middle, so remove the edge zones +# from the palm classifier to increase the usable area of the pad. +gestureProp.Palm_Edge_Zone_Width = 0 +gestureProp.Tap_Exclusion_Border_Width = 0 diff --git a/graphics/java/android/framework_graphics.aconfig b/graphics/java/android/framework_graphics.aconfig index 6c81a608241c..1e41b4d9ed1b 100644 --- a/graphics/java/android/framework_graphics.aconfig +++ b/graphics/java/android/framework_graphics.aconfig @@ -2,6 +2,7 @@ package: "com.android.graphics.flags" flag { name: "exact_compute_bounds" + is_exported: true namespace: "core_graphics" description: "Add a function without unused exact param for computeBounds." bug: "304478551" @@ -9,6 +10,7 @@ flag { flag { name: "yuv_image_compress_to_ultra_hdr" + is_exported: true namespace: "core_graphics" description: "Feature flag for YUV image compress to Ultra HDR." bug: "308978825" diff --git a/keystore/java/android/security/AndroidKeyStoreMaintenance.java b/keystore/java/android/security/AndroidKeyStoreMaintenance.java index 2430e8d8e662..efbbfc23736f 100644 --- a/keystore/java/android/security/AndroidKeyStoreMaintenance.java +++ b/keystore/java/android/security/AndroidKeyStoreMaintenance.java @@ -175,20 +175,6 @@ public class AndroidKeyStoreMaintenance { } /** - * Informs Keystore 2.0 that an off body event was detected. - */ - public static void onDeviceOffBody() { - StrictMode.noteDiskWrite(); - try { - getService().onDeviceOffBody(); - } catch (Exception e) { - // TODO This fails open. This is not a regression with respect to keystore1 but it - // should get fixed. - Log.e(TAG, "Error while reporting device off body event.", e); - } - } - - /** * Migrates a key given by the source descriptor to the location designated by the destination * descriptor. * diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java index bd9abec22325..2cac2e150919 100644 --- a/keystore/java/android/security/KeyStore.java +++ b/keystore/java/android/security/KeyStore.java @@ -17,7 +17,6 @@ package android.security; import android.compat.annotation.UnsupportedAppUsage; -import android.os.Build; import android.os.StrictMode; /** @@ -30,10 +29,6 @@ import android.os.StrictMode; */ public class KeyStore { - // ResponseCodes - see system/security/keystore/include/keystore/keystore.h - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int NO_ERROR = 1; - // Used for UID field to indicate the calling UID. public static final int UID_SELF = -1; @@ -48,19 +43,12 @@ public class KeyStore { * Add an authentication record to the keystore authorization table. * * @param authToken The packed bytes of a hw_auth_token_t to be provided to keymaster. - * @return {@code KeyStore.NO_ERROR} on success, otherwise an error value corresponding to - * a {@code KeymasterDefs.KM_ERROR_} value or {@code KeyStore} ResponseCode. + * @return 0 on success, otherwise an error value corresponding to a + * {@code KeymasterDefs.KM_ERROR_} value or {@code KeyStore} ResponseCode. */ public int addAuthToken(byte[] authToken) { StrictMode.noteDiskWrite(); return Authorization.addAuthToken(authToken); } - - /** - * Notify keystore that the device went off-body. - */ - public void onDeviceOffBody() { - AndroidKeyStoreMaintenance.onDeviceOffBody(); - } } diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java index 7aecfd8d4a0d..d359a9050a0f 100644 --- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java +++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java @@ -880,9 +880,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu } /** - * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or - * signing. Encryption and signature verification will still be available when the screen is - * locked. + * Returns {@code true} if the key is authorized to be used only while the device is unlocked. * * @see Builder#setUnlockedDeviceRequired(boolean) */ @@ -1672,16 +1670,16 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu * {@link #setUserAuthenticationValidityDurationSeconds} and * {@link #setUserAuthenticationRequired}). Once the device has been removed from the * user's body, the key will be considered unauthorized and the user will need to - * re-authenticate to use it. For keys without an authentication validity period this - * parameter has no effect. - * - * <p>Similarly, on devices that do not have an on-body sensor, this parameter will have no - * effect; the device will always be considered to be "on-body" and the key will therefore - * remain authorized until the validity period ends. - * - * @param remainsValid if {@code true}, and if the device supports on-body detection, key - * will be invalidated when the device is removed from the user's body or when the - * authentication validity expires, whichever occurs first. + * re-authenticate to use it. If the device does not have an on-body sensor or the key does + * not have an authentication validity period, this parameter has no effect. + * <p> + * Since Android 12 (API level 31), this parameter has no effect even on devices that have + * an on-body sensor. A future version of Android may restore enforcement of this parameter. + * Meanwhile, it is recommended to not use it. + * + * @param remainsValid if {@code true}, and if the device supports enforcement of this + * parameter, the key will be invalidated when the device is removed from the user's body or + * when the authentication validity expires, whichever occurs first. */ @NonNull public Builder setUserAuthenticationValidWhileOnBody(boolean remainsValid) { @@ -1723,11 +1721,49 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec, UserAu } /** - * Sets whether the keystore requires the screen to be unlocked before allowing decryption - * using this key. If this is set to {@code true}, any attempt to decrypt or sign using this - * key while the screen is locked will fail. A locked device requires a PIN, password, - * biometric, or other trusted factor to access. While the screen is locked, any associated - * public key can still be used (e.g for signature verification). + * Sets whether this key is authorized to be used only while the device is unlocked. + * <p> + * The device is considered to be locked for a user when the user's apps are currently + * inaccessible and some form of lock screen authentication is required to regain access to + * them. For the full definition, see {@link KeyguardManager#isDeviceLocked()}. + * <p> + * Public key operations aren't restricted by {@code setUnlockedDeviceRequired(true)} and + * may be performed even while the device is locked. In Android 11 (API level 30) and lower, + * encryption and verification operations with symmetric keys weren't restricted either. + * <p> + * Keys that use {@code setUnlockedDeviceRequired(true)} can be imported and generated even + * while the device is locked, as long as the device has been unlocked at least once since + * the last reboot. However, such keys cannot be used (except for the unrestricted + * operations mentioned above) until the device is unlocked. Apps that need to encrypt data + * while the device is locked such that it can only be decrypted while the device is + * unlocked can generate a key and encrypt the data in software, import the key into + * Keystore using {@code setUnlockedDeviceRequired(true)}, and zeroize the original key. + * <p> + * {@code setUnlockedDeviceRequired(true)} is related to but distinct from + * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)}. + * {@code setUnlockedDeviceRequired(true)} requires that the device be unlocked, whereas + * {@code setUserAuthenticationRequired(true)} requires that a specific type of strong + * authentication has happened within a specific time period. They may be used together or + * separately; there are cases in which one requirement can be satisfied but not the other. + * <p> + * <b>Warning:</b> Be careful using {@code setUnlockedDeviceRequired(true)} on Android 14 + * (API level 34) and lower, since the following bugs existed in Android 12 through 14: + * <ul> + * <li>When the user didn't have a secure lock screen, unlocked-device-required keys + * couldn't be generated, imported, or used.</li> + * <li>When the user's secure lock screen was removed, all of that user's + * unlocked-device-required keys were automatically deleted.</li> + * <li>Unlocking the device with a non-strong biometric, such as face on many devices, + * didn't re-authorize the use of unlocked-device-required keys.</li> + * <li>Unlocking the device with a biometric didn't re-authorize the use of + * unlocked-device-required keys in profiles that share their parent user's lock.</li> + * </ul> + * These issues are fixed in Android 15, so apps can avoid them by using + * {@code setUnlockedDeviceRequired(true)} only on Android 15 and higher. + * Apps that use both {@code setUnlockedDeviceRequired(true)} and + * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)} + * are unaffected by the first two issues, since the first two issues describe expected + * behavior for {@code setUserAuthenticationRequired(true)}. */ @NonNull public Builder setUnlockedDeviceRequired(boolean unlockedDeviceRequired) { diff --git a/keystore/java/android/security/keystore/KeyInfo.java b/keystore/java/android/security/keystore/KeyInfo.java index 5cffe46936a2..2163ca2f8217 100644 --- a/keystore/java/android/security/keystore/KeyInfo.java +++ b/keystore/java/android/security/keystore/KeyInfo.java @@ -279,7 +279,7 @@ public class KeyInfo implements KeySpec { } /** - * Returns {@code true} if the key is authorized to be used only when the device is unlocked. + * Returns {@code true} if the key is authorized to be used only while the device is unlocked. * * <p>This authorization applies only to secret key and private key operations. Public key * operations are not restricted. diff --git a/keystore/java/android/security/keystore/KeyProtection.java b/keystore/java/android/security/keystore/KeyProtection.java index 31b4a5eac619..8e5ac45d394d 100644 --- a/keystore/java/android/security/keystore/KeyProtection.java +++ b/keystore/java/android/security/keystore/KeyProtection.java @@ -577,9 +577,7 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs { } /** - * Returns {@code true} if the screen must be unlocked for this key to be used for decryption or - * signing. Encryption and signature verification will still be available when the screen is - * locked. + * Returns {@code true} if the key is authorized to be used only while the device is unlocked. * * @see Builder#setUnlockedDeviceRequired(boolean) */ @@ -1039,16 +1037,16 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs { * {@link #setUserAuthenticationValidityDurationSeconds} and * {@link #setUserAuthenticationRequired}). Once the device has been removed from the * user's body, the key will be considered unauthorized and the user will need to - * re-authenticate to use it. For keys without an authentication validity period this - * parameter has no effect. + * re-authenticate to use it. If the device does not have an on-body sensor or the key does + * not have an authentication validity period, this parameter has no effect. + * <p> + * Since Android 12 (API level 31), this parameter has no effect even on devices that have + * an on-body sensor. A future version of Android may restore enforcement of this parameter. + * Meanwhile, it is recommended to not use it. * - * <p>Similarly, on devices that do not have an on-body sensor, this parameter will have no - * effect; the device will always be considered to be "on-body" and the key will therefore - * remain authorized until the validity period ends. - * - * @param remainsValid if {@code true}, and if the device supports on-body detection, key - * will be invalidated when the device is removed from the user's body or when the - * authentication validity expires, whichever occurs first. + * @param remainsValid if {@code true}, and if the device supports enforcement of this + * parameter, the key will be invalidated when the device is removed from the user's body or + * when the authentication validity expires, whichever occurs first. */ @NonNull public Builder setUserAuthenticationValidWhileOnBody(boolean remainsValid) { @@ -1117,11 +1115,49 @@ public final class KeyProtection implements ProtectionParameter, UserAuthArgs { } /** - * Sets whether the keystore requires the screen to be unlocked before allowing decryption - * using this key. If this is set to {@code true}, any attempt to decrypt or sign using this - * key while the screen is locked will fail. A locked device requires a PIN, password, - * biometric, or other trusted factor to access. While the screen is locked, the key can - * still be used for encryption or signature verification. + * Sets whether this key is authorized to be used only while the device is unlocked. + * <p> + * The device is considered to be locked for a user when the user's apps are currently + * inaccessible and some form of lock screen authentication is required to regain access to + * them. For the full definition, see {@link KeyguardManager#isDeviceLocked()}. + * <p> + * Public key operations aren't restricted by {@code setUnlockedDeviceRequired(true)} and + * may be performed even while the device is locked. In Android 11 (API level 30) and lower, + * encryption and verification operations with symmetric keys weren't restricted either. + * <p> + * Keys that use {@code setUnlockedDeviceRequired(true)} can be imported and generated even + * while the device is locked, as long as the device has been unlocked at least once since + * the last reboot. However, such keys cannot be used (except for the unrestricted + * operations mentioned above) until the device is unlocked. Apps that need to encrypt data + * while the device is locked such that it can only be decrypted while the device is + * unlocked can generate a key and encrypt the data in software, import the key into + * Keystore using {@code setUnlockedDeviceRequired(true)}, and zeroize the original key. + * <p> + * {@code setUnlockedDeviceRequired(true)} is related to but distinct from + * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)}. + * {@code setUnlockedDeviceRequired(true)} requires that the device be unlocked, whereas + * {@code setUserAuthenticationRequired(true)} requires that a specific type of strong + * authentication has happened within a specific time period. They may be used together or + * separately; there are cases in which one requirement can be satisfied but not the other. + * <p> + * <b>Warning:</b> Be careful using {@code setUnlockedDeviceRequired(true)} on Android 14 + * (API level 34) and lower, since the following bugs existed in Android 12 through 14: + * <ul> + * <li>When the user didn't have a secure lock screen, unlocked-device-required keys + * couldn't be generated, imported, or used.</li> + * <li>When the user's secure lock screen was removed, all of that user's + * unlocked-device-required keys were automatically deleted.</li> + * <li>Unlocking the device with a non-strong biometric, such as face on many devices, + * didn't re-authorize the use of unlocked-device-required keys.</li> + * <li>Unlocking the device with a biometric didn't re-authorize the use of + * unlocked-device-required keys in profiles that share their parent user's lock.</li> + * </ul> + * These issues are fixed in Android 15, so apps can avoid them by using + * {@code setUnlockedDeviceRequired(true)} only on Android 15 and higher. + * Apps that use both {@code setUnlockedDeviceRequired(true)} and + * {@link #setUserAuthenticationRequired(boolean) setUserAuthenticationRequired(true)} + * are unaffected by the first two issues, since the first two issues describe expected + * behavior for {@code setUserAuthenticationRequired(true)}. */ @NonNull public Builder setUnlockedDeviceRequired(boolean unlockedDeviceRequired) { diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java b/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java index 101a10e3d312..3f39eeb0cc6b 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreCipherSpiBase.java @@ -359,14 +359,12 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor } catch (KeyStoreException keyStoreException) { GeneralSecurityException e = KeyStoreCryptoOperationUtils.getExceptionForCipherInit( mKey, keyStoreException); - if (e != null) { - if (e instanceof InvalidKeyException) { - throw (InvalidKeyException) e; - } else if (e instanceof InvalidAlgorithmParameterException) { - throw (InvalidAlgorithmParameterException) e; - } else { - throw new ProviderException("Unexpected exception type", e); - } + if (e instanceof InvalidKeyException) { + throw (InvalidKeyException) e; + } else if (e instanceof InvalidAlgorithmParameterException) { + throw (InvalidAlgorithmParameterException) e; + } else { + throw new ProviderException("Unexpected exception type", e); } } diff --git a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java index 372e4cb3d72e..9b82206e5709 100644 --- a/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java +++ b/keystore/java/android/security/keystore2/KeyStoreCryptoOperationUtils.java @@ -20,7 +20,6 @@ import android.app.ActivityThread; import android.hardware.biometrics.BiometricManager; import android.hardware.security.keymint.ErrorCode; import android.security.GateKeeper; -import android.security.KeyStore; import android.security.KeyStoreException; import android.security.KeyStoreOperation; import android.security.keymaster.KeymasterDefs; @@ -131,15 +130,10 @@ abstract class KeyStoreCryptoOperationUtils { /** * Returns the exception to be thrown by the {@code Cipher.init} method of the crypto operation - * in response to {@code KeyStore.begin} operation or {@code null} if the {@code init} method - * should succeed. + * in response to a failed {code IKeystoreSecurityLevel#createOperation()}. */ public static GeneralSecurityException getExceptionForCipherInit( AndroidKeyStoreKey key, KeyStoreException e) { - if (e.getErrorCode() == KeyStore.NO_ERROR) { - return null; - } - // Cipher-specific cases switch (e.getErrorCode()) { case KeymasterDefs.KM_ERROR_INVALID_NONCE: diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index 97562783882c..16c77d0c3c81 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -53,7 +53,7 @@ class WindowExtensionsImpl implements WindowExtensions { * The min version of the WM Extensions that must be supported in the current platform version. */ @VisibleForTesting - static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 5; + static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 6; private final Object mLock = new Object(); private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index 100185b84b77..cae232e54f3c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -17,6 +17,12 @@ package androidx.window.extensions.embedding; import static android.util.TypedValue.COMPLEX_UNIT_DIP; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; +import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; import static androidx.window.extensions.embedding.DividerAttributes.RATIO_UNSET; import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_UNSET; @@ -28,34 +34,253 @@ import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSI import android.annotation.Nullable; import android.app.ActivityThread; import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RotateDrawable; +import android.hardware.display.DisplayManager; +import android.os.IBinder; import android.util.TypedValue; +import android.view.Gravity; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.window.InputTransferToken; +import android.window.TaskFragmentOperation; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; +import androidx.annotation.IdRes; import androidx.annotation.NonNull; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.window.flags.Flags; +import java.util.Objects; + /** * Manages the rendering and interaction of the divider. */ class DividerPresenter { + private static final String WINDOW_NAME = "AE Divider"; + // TODO(b/327067596) Update based on UX guidance. - @VisibleForTesting static final float DEFAULT_MIN_RATIO = 0.35f; - @VisibleForTesting static final float DEFAULT_MAX_RATIO = 0.65f; - @VisibleForTesting static final int DEFAULT_DIVIDER_WIDTH_DP = 24; + private static final Color DEFAULT_DIVIDER_COLOR = Color.valueOf(Color.BLACK); + @VisibleForTesting + static final float DEFAULT_MIN_RATIO = 0.35f; + @VisibleForTesting + static final float DEFAULT_MAX_RATIO = 0.65f; + @VisibleForTesting + static final int DEFAULT_DIVIDER_WIDTH_DP = 24; + + /** + * The {@link Properties} of the divider. This field is {@code null} when no divider should be + * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface + * is not available. + */ + @Nullable + @VisibleForTesting + Properties mProperties; + + /** + * The {@link Renderer} of the divider. This field is {@code null} when no divider should be + * drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or + * updated when {@link #mProperties} is changed. + */ + @Nullable + @VisibleForTesting + Renderer mRenderer; + + /** + * The owner TaskFragment token of the decor surface. The decor surface is placed right above + * the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed. + */ + @Nullable + @VisibleForTesting + IBinder mDecorSurfaceOwner; + + /** Updates the divider when external conditions are changed. */ + void updateDivider( + @NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentParentInfo parentInfo, + @Nullable SplitContainer topSplitContainer) { + if (!Flags.activityEmbeddingInteractiveDividerFlag()) { + return; + } + + // Clean up the decor surface if top SplitContainer is null. + if (topSplitContainer == null) { + removeDecorSurfaceAndDivider(wct); + return; + } + + // Clean up the decor surface if DividerAttributes is null. + final DividerAttributes dividerAttributes = + topSplitContainer.getCurrentSplitAttributes().getDividerAttributes(); + if (dividerAttributes == null) { + removeDecorSurfaceAndDivider(wct); + return; + } + + if (topSplitContainer.getCurrentSplitAttributes().getSplitType() + instanceof SplitAttributes.SplitType.ExpandContainersSplitType) { + // No divider is needed for ExpandContainersSplitType. + removeDivider(); + return; + } + + // Skip updating when the TFs have not been updated to match the SplitAttributes. + if (topSplitContainer.getPrimaryContainer().getLastRequestedBounds().isEmpty() + || topSplitContainer.getSecondaryContainer().getLastRequestedBounds().isEmpty()) { + return; + } + + final SurfaceControl decorSurface = parentInfo.getDecorSurface(); + if (decorSurface == null) { + // Clean up when the decor surface is currently unavailable. + removeDivider(); + // Request to create the decor surface + createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer()); + return; + } + + // make the top primary container the owner of the decor surface. + if (!Objects.equals(mDecorSurfaceOwner, + topSplitContainer.getPrimaryContainer().getTaskFragmentToken())) { + createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer()); + } + + updateProperties( + new Properties( + parentInfo.getConfiguration(), + dividerAttributes, + decorSurface, + getInitialDividerPosition(topSplitContainer), + isVerticalSplit(topSplitContainer), + parentInfo.getDisplayId())); + } + + private void updateProperties(@NonNull Properties properties) { + if (Properties.equalsForDivider(mProperties, properties)) { + return; + } + final Properties previousProperties = mProperties; + mProperties = properties; + + if (mRenderer == null) { + // Create a new renderer when a renderer doesn't exist yet. + mRenderer = new Renderer(); + } else if (!Properties.areSameSurfaces( + previousProperties.mDecorSurface, mProperties.mDecorSurface) + || previousProperties.mDisplayId != mProperties.mDisplayId) { + // Release and recreate the renderer if the decor surface or the display has changed. + mRenderer.release(); + mRenderer = new Renderer(); + } else { + // Otherwise, update the renderer for the new properties. + mRenderer.update(); + } + } + + /** + * Creates a decor surface for the TaskFragment if no decor surface exists, or changes the owner + * of the existing decor surface to be the specified TaskFragment. + * + * See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}. + */ + private void createOrMoveDecorSurface( + @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + wct.addTaskFragmentOperation(container.getTaskFragmentToken(), operation); + mDecorSurfaceOwner = container.getTaskFragmentToken(); + } + + private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) { + if (mDecorSurfaceOwner != null) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation); + mDecorSurfaceOwner = null; + } + removeDivider(); + } + + private void removeDivider() { + if (mRenderer != null) { + mRenderer.release(); + } + mProperties = null; + mRenderer = null; + } + + @VisibleForTesting + static int getInitialDividerPosition(@NonNull SplitContainer splitContainer) { + final Rect primaryBounds = + splitContainer.getPrimaryContainer().getLastRequestedBounds(); + final Rect secondaryBounds = + splitContainer.getSecondaryContainer().getLastRequestedBounds(); + if (isVerticalSplit(splitContainer)) { + return Math.min(primaryBounds.right, secondaryBounds.right); + } else { + return Math.min(primaryBounds.bottom, secondaryBounds.bottom); + } + } + + private static boolean isVerticalSplit(@NonNull SplitContainer splitContainer) { + final int layoutDirection = splitContainer.getCurrentSplitAttributes().getLayoutDirection(); + switch(layoutDirection) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: + case SplitAttributes.LayoutDirection.LOCALE: + return true; + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return false; + default: + throw new IllegalArgumentException("Invalid layout direction:" + layoutDirection); + } + } - static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) { + private static void safeReleaseSurfaceControl(@Nullable SurfaceControl sc) { + if (sc != null) { + sc.release(); + } + } + + private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) { int dividerWidthDp = dividerAttributes.getWidthDp(); + return convertDpToPixel(dividerWidthDp); + } + private static int convertDpToPixel(int dp) { // TODO(b/329193115) support divider on secondary display final Context applicationContext = ActivityThread.currentActivityThread().getApplication(); return (int) TypedValue.applyDimension( COMPLEX_UNIT_DIP, - dividerWidthDp, + dp, applicationContext.getResources().getDisplayMetrics()); } + private static int getDimensionDp(@IdRes int resId) { + final Context context = ActivityThread.currentActivityThread().getApplication(); + final int px = context.getResources().getDimensionPixelSize(resId); + return (int) TypedValue.convertPixelsToDimension( + COMPLEX_UNIT_DIP, + px, + context.getResources().getDisplayMetrics()); + } + /** * Returns the container bound offset that is a result of the presence of a divider. * @@ -140,6 +365,12 @@ class DividerPresenter { widthDp = DEFAULT_DIVIDER_WIDTH_DP; } + if (dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + // Draggable divider width must be larger than the drag handle size. + widthDp = Math.max(widthDp, + getDimensionDp(R.dimen.activity_embedding_divider_touch_target_width)); + } + float minRatio = dividerAttributes.getPrimaryMinRatio(); if (minRatio == RATIO_UNSET) { minRatio = DEFAULT_MIN_RATIO; @@ -156,4 +387,231 @@ class DividerPresenter { .setPrimaryMaxRatio(maxRatio) .build(); } + + /** + * Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on + * these properties. When any value is updated, the divider is re-rendered. The Properties + * instance is created only when all the pre-conditions of drawing a divider are met. + */ + @VisibleForTesting + static class Properties { + private static final int CONFIGURATION_MASK_FOR_DIVIDER = + ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_WINDOW_CONFIGURATION; + @NonNull + private final Configuration mConfiguration; + @NonNull + private final DividerAttributes mDividerAttributes; + @NonNull + private final SurfaceControl mDecorSurface; + + /** The initial position of the divider calculated based on container bounds. */ + private final int mInitialDividerPosition; + + /** Whether the split is vertical, such as left-to-right or right-to-left split. */ + private final boolean mIsVerticalSplit; + + private final int mDisplayId; + + @VisibleForTesting + Properties( + @NonNull Configuration configuration, + @NonNull DividerAttributes dividerAttributes, + @NonNull SurfaceControl decorSurface, + int initialDividerPosition, + boolean isVerticalSplit, + int displayId) { + mConfiguration = configuration; + mDividerAttributes = dividerAttributes; + mDecorSurface = decorSurface; + mInitialDividerPosition = initialDividerPosition; + mIsVerticalSplit = isVerticalSplit; + mDisplayId = displayId; + } + + /** + * Compares whether two Properties objects are equal for rendering the divider. The + * Configuration is checked for rendering related fields, and other fields are checked for + * regular equality. + */ + private static boolean equalsForDivider(@Nullable Properties a, @Nullable Properties b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + return areSameSurfaces(a.mDecorSurface, b.mDecorSurface) + && Objects.equals(a.mDividerAttributes, b.mDividerAttributes) + && areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration) + && a.mInitialDividerPosition == b.mInitialDividerPosition + && a.mIsVerticalSplit == b.mIsVerticalSplit + && a.mDisplayId == b.mDisplayId; + } + + private static boolean areSameSurfaces( + @Nullable SurfaceControl sc1, @Nullable SurfaceControl sc2) { + if (sc1 == sc2) { + // If both are null or both refer to the same object. + return true; + } + if (sc1 == null || sc2 == null) { + return false; + } + return sc1.isSameSurface(sc2); + } + + private static boolean areConfigurationsEqualForDivider( + @NonNull Configuration a, @NonNull Configuration b) { + final int diff = a.diff(b); + return (diff & CONFIGURATION_MASK_FOR_DIVIDER) == 0; + } + } + + /** + * Handles the rendering of the divider. When the decor surface is updated, the renderer is + * recreated. When other fields in the Properties are changed, the renderer is updated. + */ + @VisibleForTesting + class Renderer { + @NonNull + private final SurfaceControl mDividerSurface; + @NonNull + private final WindowlessWindowManager mWindowlessWindowManager; + @NonNull + private final SurfaceControlViewHost mViewHost; + @NonNull + private final FrameLayout mDividerLayout; + private final int mDividerWidthPx; + + private Renderer() { + mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes); + + mDividerSurface = createChildSurface("DividerSurface", true /* visible */); + mWindowlessWindowManager = new WindowlessWindowManager( + mProperties.mConfiguration, + mDividerSurface, + new InputTransferToken()); + + final Context context = ActivityThread.currentActivityThread().getApplication(); + final DisplayManager displayManager = context.getSystemService(DisplayManager.class); + mViewHost = new SurfaceControlViewHost( + context, displayManager.getDisplay(mProperties.mDisplayId), + mWindowlessWindowManager, "DividerContainer"); + mDividerLayout = new FrameLayout(context); + + update(); + } + + /** Updates the divider when properties are changed */ + @VisibleForTesting + void update() { + mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration); + updateSurface(); + updateLayout(); + updateDivider(); + } + + @VisibleForTesting + void release() { + mViewHost.release(); + // TODO handle synchronization between surface transactions and WCT. + new SurfaceControl.Transaction().remove(mDividerSurface).apply(); + safeReleaseSurfaceControl(mDividerSurface); + } + + private void updateSurface() { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + // TODO handle synchronization between surface transactions and WCT. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + if (mProperties.mIsVerticalSplit) { + t.setPosition(mDividerSurface, mProperties.mInitialDividerPosition, 0.0f); + t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height()); + } else { + t.setPosition(mDividerSurface, 0.0f, mProperties.mInitialDividerPosition); + t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx); + } + t.apply(); + } + + private void updateLayout() { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit + ? new WindowManager.LayoutParams( + mDividerWidthPx, + taskBounds.height(), + TYPE_APPLICATION_PANEL, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, + PixelFormat.TRANSLUCENT) + : new WindowManager.LayoutParams( + taskBounds.width(), + mDividerWidthPx, + TYPE_APPLICATION_PANEL, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, + PixelFormat.TRANSLUCENT); + lp.setTitle(WINDOW_NAME); + mViewHost.setView(mDividerLayout, lp); + } + + private void updateDivider() { + mDividerLayout.removeAllViews(); + mDividerLayout.setBackgroundColor(DEFAULT_DIVIDER_COLOR.toArgb()); + if (mProperties.mDividerAttributes.getDividerType() + == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + drawDragHandle(); + } + mViewHost.getView().invalidate(); + } + + private void drawDragHandle() { + final Context context = mDividerLayout.getContext(); + final ImageButton button = new ImageButton(context); + final FrameLayout.LayoutParams params = mProperties.mIsVerticalSplit + ? new FrameLayout.LayoutParams( + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_width), + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_height)) + : new FrameLayout.LayoutParams( + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_height), + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_width)); + params.gravity = Gravity.CENTER; + button.setLayoutParams(params); + button.setBackgroundColor(R.color.transparent); + + final Drawable handle = context.getResources().getDrawable( + R.drawable.activity_embedding_divider_handle, context.getTheme()); + if (mProperties.mIsVerticalSplit) { + button.setImageDrawable(handle); + } else { + // Rotate the handle drawable + RotateDrawable rotatedHandle = new RotateDrawable(); + rotatedHandle.setFromDegrees(90f); + rotatedHandle.setToDegrees(90f); + rotatedHandle.setPivotXRelative(true); + rotatedHandle.setPivotYRelative(true); + rotatedHandle.setPivotX(0.5f); + rotatedHandle.setPivotY(0.5f); + rotatedHandle.setLevel(1); + rotatedHandle.setDrawable(handle); + + button.setImageDrawable(rotatedHandle); + } + mDividerLayout.addView(button); + } + + @NonNull + private SurfaceControl createChildSurface(@NonNull String name, boolean visible) { + final Rect bounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + return new SurfaceControl.Builder() + .setParent(mProperties.mDecorSurface) + .setName(name) + .setHidden(!visible) + .setCallsite("DividerManager.createChildSurface") + .setBufferSize(bounds.width(), bounds.height()) + .setColorLayer() + .build(); + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 80afb16d5832..3f4dddf0cc81 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -168,11 +168,14 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { * @param fragmentToken token of an existing TaskFragment. */ void expandTaskFragment(@NonNull WindowContainerTransaction wct, - @NonNull IBinder fragmentToken) { + @NonNull TaskFragmentContainer container) { + final IBinder fragmentToken = container.getTaskFragmentToken(); resizeTaskFragment(wct, fragmentToken, new Rect()); clearAdjacentTaskFragments(wct, fragmentToken); updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED); updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); + + container.getTaskContainer().updateDivider(wct); } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 0cc4b1f367d8..1bc8264d8e7e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -844,6 +844,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Checks if container should be updated before apply new parentInfo. final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo); taskContainer.updateTaskFragmentParentInfo(parentInfo); + taskContainer.updateDivider(wct); // If the last direct activity of the host task is dismissed and the overlay container is // the only taskFragment, the overlay container should also be dismissed. @@ -1224,7 +1225,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TaskFragmentContainer container = getContainerWithActivity(activity); if (shouldContainerBeExpanded(container)) { // Make sure that the existing container is expanded. - mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); + mPresenter.expandTaskFragment(wct, container); } else { // Put activity into a new expanded container. final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity)); @@ -1928,7 +1929,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } if (shouldContainerBeExpanded(container)) { if (container.getInfo() != null) { - mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); + mPresenter.expandTaskFragment(wct, container); } // If the info is not available yet the task fragment will be expanded when it's ready return; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index f680694c3af9..20bc82002339 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -368,6 +368,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes); updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); + taskContainer.updateDivider(wct); } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @@ -686,8 +687,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { splitContainer.getPrimaryContainer().getTaskFragmentToken(); final IBinder secondaryToken = splitContainer.getSecondaryContainer().getTaskFragmentToken(); - expandTaskFragment(wct, primaryToken); - expandTaskFragment(wct, secondaryToken); + expandTaskFragment(wct, splitContainer.getPrimaryContainer()); + expandTaskFragment(wct, splitContainer.getSecondaryContainer()); // Set the companion TaskFragment when the two containers stacked. setCompanionTaskFragment(wct, primaryToken, secondaryToken, splitContainer.getSplitRule(), true /* isStacked */); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 73109e266905..e75a317cc3b3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -77,6 +77,9 @@ class TaskContainer { private boolean mHasDirectActivity; + @Nullable + private TaskFragmentParentInfo mTaskFragmentParentInfo; + /** * TaskFragments that the organizer has requested to be closed. They should be removed when * the organizer receives @@ -85,14 +88,17 @@ class TaskContainer { */ final Set<IBinder> mFinishedContainer = new ArraySet<>(); + // TODO(b/293654166): move DividerPresenter to SplitController. + @NonNull + final DividerPresenter mDividerPresenter; + /** * The {@link TaskContainer} constructor * - * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with - * {@code activityInTask}. + * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with + * {@code activityInTask}. * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to * initialize the {@link TaskContainer} properties. - * */ TaskContainer(int taskId, @NonNull Activity activityInTask) { if (taskId == INVALID_TASK_ID) { @@ -107,6 +113,7 @@ class TaskContainer { // the host task is visible and has an activity in the task. mIsVisible = true; mHasDirectActivity = true; + mDividerPresenter = new DividerPresenter(); } int getTaskId() { @@ -136,10 +143,12 @@ class TaskContainer { } void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { + // TODO(b/293654166): cache the TaskFragmentParentInfo and remove these fields. mConfiguration.setTo(info.getConfiguration()); mDisplayId = info.getDisplayId(); mIsVisible = info.isVisible(); mHasDirectActivity = info.hasDirectActivity(); + mTaskFragmentParentInfo = info; } /** @@ -161,8 +170,8 @@ class TaskContainer { * Returns the windowing mode for the TaskFragments below this Task, which should be split with * other TaskFragments. * - * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when - * the pair of TaskFragments are stacked due to the limited space. + * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when + * the pair of TaskFragments are stacked due to the limited space. */ @WindowingMode int getWindowingModeForTaskFragment(@Nullable Rect taskFragmentBounds) { @@ -228,7 +237,7 @@ class TaskContainer { @Nullable TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin, - boolean includeOverlay) { + boolean includeOverlay) { for (int i = mContainers.size() - 1; i >= 0; i--) { final TaskFragmentContainer container = mContainers.get(i); if (!includePin && isTaskFragmentContainerPinned(container)) { @@ -283,7 +292,7 @@ class TaskContainer { return mContainers.indexOf(child); } - /** Whether the Task is in an intermediate state waiting for the server update.*/ + /** Whether the Task is in an intermediate state waiting for the server update. */ boolean isInIntermediateState() { for (TaskFragmentContainer container : mContainers) { if (container.isInIntermediateState()) { @@ -389,6 +398,26 @@ class TaskContainer { return mContainers; } + void updateDivider(@NonNull WindowContainerTransaction wct) { + if (mTaskFragmentParentInfo != null) { + // Update divider only if TaskFragmentParentInfo is available. + mDividerPresenter.updateDivider( + wct, mTaskFragmentParentInfo, getTopNonFinishingSplitContainer()); + } + } + + @Nullable + private SplitContainer getTopNonFinishingSplitContainer() { + for (int i = mSplitContainers.size() - 1; i >= 0; i--) { + final SplitContainer splitContainer = mSplitContainers.get(i); + if (!splitContainer.getPrimaryContainer().isFinished() + && !splitContainer.getSecondaryContainer().isFinished()) { + return splitContainer; + } + } + return null; + } + private void onTaskFragmentContainerUpdated() { // TODO(b/300211704): Find a better mechanism to handle the z-order in case we introduce // another special container that should also be on top in the future. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index a6bf99d4add5..e20a3e02c65d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -748,6 +748,10 @@ class TaskFragmentContainer { } } + @NonNull Rect getLastRequestedBounds() { + return mLastRequestedBounds; + } + /** * Checks if last requested windowing mode is equal to the provided value. * @see WindowContainerTransaction#setWindowingMode diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java index 2a277f4c9619..4d1d807038eb 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -16,22 +16,49 @@ package androidx.window.extensions.embedding; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; + import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider; +import static androidx.window.extensions.embedding.DividerPresenter.getInitialDividerPosition; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Binder; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.Display; +import android.view.SurfaceControl; +import android.window.TaskFragmentOperation; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; + +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Test class for {@link DividerPresenter}. @@ -43,6 +70,167 @@ import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) public class DividerPresenterTest { + @Rule + public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); + + @Mock + private DividerPresenter.Renderer mRenderer; + + @Mock + private WindowContainerTransaction mTransaction; + + @Mock + private TaskFragmentParentInfo mParentInfo; + + @Mock + private SplitContainer mSplitContainer; + + @Mock + private SurfaceControl mSurfaceControl; + + private DividerPresenter mDividerPresenter; + + private final IBinder mPrimaryContainerToken = new Binder(); + + private final IBinder mSecondaryContainerToken = new Binder(); + + private final IBinder mAnotherContainerToken = new Binder(); + + private DividerPresenter.Properties mProperties; + + private static final DividerAttributes DEFAULT_DIVIDER_ATTRIBUTES = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE).build(); + + private static final DividerAttributes ANOTHER_DIVIDER_ATTRIBUTES = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) + .setWidthDp(10).build(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG); + + when(mParentInfo.getDisplayId()).thenReturn(Display.DEFAULT_DISPLAY); + when(mParentInfo.getConfiguration()).thenReturn(new Configuration()); + when(mParentInfo.getDecorSurface()).thenReturn(mSurfaceControl); + + when(mSplitContainer.getCurrentSplitAttributes()).thenReturn( + new SplitAttributes.Builder() + .setDividerAttributes(DEFAULT_DIVIDER_ATTRIBUTES) + .build()); + final TaskFragmentContainer mockPrimaryContainer = + createMockTaskFragmentContainer( + mPrimaryContainerToken, new Rect(0, 0, 950, 1000)); + final TaskFragmentContainer mockSecondaryContainer = + createMockTaskFragmentContainer( + mSecondaryContainerToken, new Rect(1000, 0, 2000, 1000)); + when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer); + when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer); + + mProperties = new DividerPresenter.Properties( + new Configuration(), + DEFAULT_DIVIDER_ATTRIBUTES, + mSurfaceControl, + getInitialDividerPosition(mSplitContainer), + true /* isVerticalSplit */, + Display.DEFAULT_DISPLAY); + + mDividerPresenter = new DividerPresenter(); + mDividerPresenter.mProperties = mProperties; + mDividerPresenter.mRenderer = mRenderer; + mDividerPresenter.mDecorSurfaceOwner = mPrimaryContainerToken; + } + + @Test + public void testUpdateDivider() { + when(mSplitContainer.getCurrentSplitAttributes()).thenReturn( + new SplitAttributes.Builder() + .setDividerAttributes(ANOTHER_DIVIDER_ATTRIBUTES) + .build()); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + + assertNotEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer).update(); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); + } + + @Test + public void testUpdateDivider_updateDecorSurfaceOwnerIfPrimaryContainerChanged() { + final TaskFragmentContainer mockPrimaryContainer = + createMockTaskFragmentContainer( + mAnotherContainerToken, new Rect(0, 0, 750, 1000)); + final TaskFragmentContainer mockSecondaryContainer = + createMockTaskFragmentContainer( + mSecondaryContainerToken, new Rect(800, 0, 2000, 1000)); + when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer); + when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + + assertNotEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer).update(); + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + assertEquals(mAnotherContainerToken, mDividerPresenter.mDecorSurfaceOwner); + verify(mTransaction).addTaskFragmentOperation(mAnotherContainerToken, operation); + } + + @Test + public void testUpdateDivider_noChangeIfPropertiesIdentical() { + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + + assertEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer, never()).update(); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); + } + + @Test + public void testUpdateDivider_dividerRemovedWhenSplitContainerIsNull() { + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + null /* splitContainer */); + final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + + verify(mTransaction).addTaskFragmentOperation( + mPrimaryContainerToken, taskFragmentOperation); + verify(mRenderer).release(); + assertNull(mDividerPresenter.mRenderer); + assertNull(mDividerPresenter.mProperties); + assertNull(mDividerPresenter.mDecorSurfaceOwner); + } + + @Test + public void testUpdateDivider_dividerRemovedWhenDividerAttributesIsNull() { + when(mSplitContainer.getCurrentSplitAttributes()).thenReturn( + new SplitAttributes.Builder().setDividerAttributes(null).build()); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + + verify(mTransaction).addTaskFragmentOperation( + mPrimaryContainerToken, taskFragmentOperation); + verify(mRenderer).release(); + assertNull(mDividerPresenter.mRenderer); + assertNull(mDividerPresenter.mProperties); + assertNull(mDividerPresenter.mDecorSurfaceOwner); + } + @Test public void testSanitizeDividerAttributes_setDefaultValues() { DividerAttributes attributes = @@ -61,7 +249,7 @@ public class DividerPresenterTest { public void testSanitizeDividerAttributes_notChangingValidValues() { DividerAttributes attributes = new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) - .setWidthDp(10) + .setWidthDp(24) .setPrimaryMinRatio(0.3f) .setPrimaryMaxRatio(0.7f) .build(); @@ -123,6 +311,14 @@ public class DividerPresenterTest { dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset); } + private TaskFragmentContainer createMockTaskFragmentContainer( + @NonNull IBinder token, @NonNull Rect bounds) { + final TaskFragmentContainer container = mock(TaskFragmentContainer.class); + when(container.getTaskFragmentToken()).thenReturn(token); + when(container.getLastRequestedBounds()).thenReturn(bounds); + return container; + } + private void assertDividerOffsetEquals( int dividerWidthPx, @NonNull SplitAttributes.SplitType splitType, diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index dd087e8eb7c9..6f37e9cb794d 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -107,7 +107,7 @@ public class JetpackTaskFragmentOrganizerTest { mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); container.setInfo(mTransaction, info); - mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken()); + mOrganizer.expandTaskFragment(mTransaction, container); verify(mTransaction).setWindowingMode(container.getInfo().getToken(), WINDOWING_MODE_UNDEFINED); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index cdb37acfc0c2..c246a19f27e2 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -642,7 +642,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken()); + verify(mSplitPresenter).expandTaskFragment(mTransaction, container); } @Test diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index 941b4e1c3e41..62d8aa30a576 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -665,8 +665,8 @@ public class SplitPresenterTest { assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); - verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); - verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + verify(mPresenter).expandTaskFragment(mTransaction, primaryTf); + verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf); splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES); clearInvocations(mPresenter); @@ -675,8 +675,8 @@ public class SplitPresenterTest { splitContainer, mActivity, null /* secondaryActivity */, new Intent(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class))); - verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); - verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + verify(mPresenter).expandTaskFragment(mTransaction, primaryTf); + verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf); } @Test diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp index 1686d0d54dc4..1ad19c9f3033 100644 --- a/libs/WindowManager/Shell/multivalentTests/Android.bp +++ b/libs/WindowManager/Shell/multivalentTests/Android.bp @@ -46,6 +46,7 @@ android_robolectric_test { exclude_srcs: ["src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"], static_libs: [ "junit", + "androidx.core_core-animation-testing", "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", @@ -64,6 +65,7 @@ android_test { static_libs: [ "WindowManager-Shell", "junit", + "androidx.core_core-animation-testing", "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt index e422198c40c5..e73d8802f0b2 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -26,6 +26,7 @@ import android.view.WindowManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.protolog.common.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT import com.android.wm.shell.common.bubbles.BubbleBarLocation @@ -54,6 +55,7 @@ class BubblePositionerTest { @Before fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false val windowManager = context.getSystemService(WindowManager::class.java) positioner = BubblePositioner(context, windowManager) } @@ -167,8 +169,9 @@ class BubblePositionerTest { @Test fun testGetRestingPosition_afterBoundsChange() { - positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, - windowBounds = Rect(0, 0, 2000, 1600))) + positioner.update( + defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600)) + ) // Set the resting position to the right side var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) @@ -176,8 +179,9 @@ class BubblePositionerTest { positioner.restingPosition = restingPosition // Now make the device smaller - positioner.update(defaultDeviceConfig.copy(isLargeScreen = false, - windowBounds = Rect(0, 0, 1000, 1600))) + positioner.update( + defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600)) + ) // Check the resting position is on the correct side allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) @@ -236,7 +240,8 @@ class BubblePositionerTest { 0 /* taskId */, null /* locus */, true /* isDismissable */, - directExecutor()) {} + directExecutor() + ) {} // Ensure the height is the same as the desired value assertThat(positioner.getExpandedViewHeight(bubble)) @@ -263,7 +268,8 @@ class BubblePositionerTest { 0 /* taskId */, null /* locus */, true /* isDismissable */, - directExecutor()) {} + directExecutor() + ) {} // Ensure the height is the same as the desired value val minHeight = @@ -471,20 +477,20 @@ class BubblePositionerTest { fun testGetTaskViewContentWidth_onLeft() { positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */) - val paddings = positioner.getExpandedViewContainerPadding(true /* onLeft */, - false /* isOverflow */) - assertThat(taskViewWidth).isEqualTo( - positioner.screenRect.width() - paddings[0] - paddings[2]) + val paddings = + positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */) + assertThat(taskViewWidth) + .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) } @Test fun testGetTaskViewContentWidth_onRight() { positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0))) val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */) - val paddings = positioner.getExpandedViewContainerPadding(false /* onLeft */, - false /* isOverflow */) - assertThat(taskViewWidth).isEqualTo( - positioner.screenRect.width() - paddings[0] - paddings[2]) + val paddings = + positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */) + assertThat(taskViewWidth) + .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2]) } @Test @@ -513,6 +519,66 @@ class BubblePositionerTest { assertThat(positioner.isBubbleBarOnLeft).isFalse() } + @Test + fun testGetBubbleBarExpandedViewBounds_onLeft() { + testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_onRight() { + testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() { + testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true) + } + + @Test + fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() { + testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true) + } + + private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) { + positioner.setShowingInBubbleBar(true) + val deviceConfig = + defaultDeviceConfig.copy( + isLargeScreen = true, + isLandscape = true, + insets = Insets.of(10, 20, 5, 15), + windowBounds = Rect(0, 0, 2000, 2600) + ) + positioner.update(deviceConfig) + + positioner.bubbleBarBounds = getBubbleBarBounds(onLeft, deviceConfig) + + val expandedViewPadding = + context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding) + + val left: Int + val right: Int + if (onLeft) { + // Pin to the left, calculate right + left = deviceConfig.insets.left + expandedViewPadding + right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow) + } else { + // Pin to the right, calculate left + right = + deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding + left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow) + } + // Above the bubble bar + val bottom = positioner.bubbleBarBounds.top - expandedViewPadding + // Calculate right and top based on size + val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow) + val expectedBounds = Rect(left, top, right, bottom) + + val bounds = Rect() + positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds) + + assertThat(bounds).isEqualTo(expectedBounds) + } + private val defaultYPosition: Float /** * Calculates the Y position bubbles should be placed based on the config. Based on the @@ -544,4 +610,21 @@ class BubblePositionerTest { positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent } + + private fun getBubbleBarBounds(onLeft: Boolean, deviceConfig: DeviceConfig): Rect { + val width = 200 + val height = 100 + val bottom = deviceConfig.windowBounds.bottom - deviceConfig.insets.bottom + val top = bottom - height + val left: Int + val right: Int + if (onLeft) { + left = deviceConfig.insets.left + right = left + width + } else { + right = deviceConfig.windowBounds.right - deviceConfig.insets.right + left = right - width + } + return Rect(left, top, right, bottom) + } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt new file mode 100644 index 000000000000..2ac77917a348 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles.bar + +import android.content.Context +import android.graphics.Insets +import android.graphics.Rect +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.DeviceConfig +import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_ALPHA_IN_DURATION +import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_ALPHA_OUT_DURATION +import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_SCALE +import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.ClassRule +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for [BubbleBarDropTargetController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarDropTargetControllerTest { + + companion object { + @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule() + } + + private val context = ApplicationProvider.getApplicationContext<Context>() + private lateinit var controller: BubbleBarDropTargetController + private lateinit var positioner: BubblePositioner + private lateinit var container: FrameLayout + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + container = FrameLayout(context) + val windowManager = context.getSystemService(WindowManager::class.java) + positioner = BubblePositioner(context, windowManager) + positioner.setShowingInBubbleBar(true) + val deviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, 2000, 2600), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + insets = Insets.of(10, 20, 30, 40) + ) + positioner.update(deviceConfig) + positioner.bubbleBarBounds = Rect(1800, 2400, 1970, 2560) + + controller = BubbleBarDropTargetController(context, container, positioner) + } + + @Test + fun show_moveLeftToRight_isVisibleWithExpectedBounds() { + val expectedBoundsOnLeft = getExpectedDropTargetBounds(onLeft = true) + val expectedBoundsOnRight = getExpectedDropTargetBounds(onLeft = false) + + runOnMainSync { controller.show(BubbleBarLocation.LEFT) } + waitForAnimateIn() + val viewOnLeft = getDropTargetView() + assertThat(viewOnLeft).isNotNull() + assertThat(viewOnLeft!!.alpha).isEqualTo(1f) + assertThat(viewOnLeft.layoutParams.width).isEqualTo(expectedBoundsOnLeft.width()) + assertThat(viewOnLeft.layoutParams.height).isEqualTo(expectedBoundsOnLeft.height()) + assertThat(viewOnLeft.x).isEqualTo(expectedBoundsOnLeft.left) + assertThat(viewOnLeft.y).isEqualTo(expectedBoundsOnLeft.top) + + runOnMainSync { controller.show(BubbleBarLocation.RIGHT) } + waitForAnimateOut() + waitForAnimateIn() + val viewOnRight = getDropTargetView() + assertThat(viewOnRight).isNotNull() + assertThat(viewOnRight!!.alpha).isEqualTo(1f) + assertThat(viewOnRight.layoutParams.width).isEqualTo(expectedBoundsOnRight.width()) + assertThat(viewOnRight.layoutParams.height).isEqualTo(expectedBoundsOnRight.height()) + assertThat(viewOnRight.x).isEqualTo(expectedBoundsOnRight.left) + assertThat(viewOnRight.y).isEqualTo(expectedBoundsOnRight.top) + } + + @Test + fun toggleSetHidden_dropTargetShown_updatesAlpha() { + runOnMainSync { controller.show(BubbleBarLocation.RIGHT) } + waitForAnimateIn() + val view = getDropTargetView() + assertThat(view).isNotNull() + assertThat(view!!.alpha).isEqualTo(1f) + + runOnMainSync { controller.setHidden(true) } + waitForAnimateOut() + val hiddenView = getDropTargetView() + assertThat(hiddenView).isNotNull() + assertThat(hiddenView!!.alpha).isEqualTo(0f) + + runOnMainSync { controller.setHidden(false) } + waitForAnimateIn() + val shownView = getDropTargetView() + assertThat(shownView).isNotNull() + assertThat(shownView!!.alpha).isEqualTo(1f) + } + + @Test + fun toggleSetHidden_dropTargetNotShown_viewNotCreated() { + runOnMainSync { controller.setHidden(true) } + waitForAnimateOut() + assertThat(getDropTargetView()).isNull() + runOnMainSync { controller.setHidden(false) } + waitForAnimateIn() + assertThat(getDropTargetView()).isNull() + } + + @Test + fun dismiss_dropTargetShown_viewRemoved() { + runOnMainSync { controller.show(BubbleBarLocation.LEFT) } + waitForAnimateIn() + assertThat(getDropTargetView()).isNotNull() + runOnMainSync { controller.dismiss() } + waitForAnimateOut() + assertThat(getDropTargetView()).isNull() + } + + @Test + fun dismiss_dropTargetNotShown_doesNothing() { + runOnMainSync { controller.dismiss() } + waitForAnimateOut() + assertThat(getDropTargetView()).isNull() + } + + private fun getDropTargetView(): View? = container.findViewById(R.id.bubble_bar_drop_target) + + private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect { + val rect = Rect() + positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, rect) + // Scale the rect to expected size, but keep the center point the same + val centerX = rect.centerX() + val centerY = rect.centerY() + rect.scale(DROP_TARGET_SCALE) + rect.offset(centerX - rect.centerX(), centerY - rect.centerY()) + return rect + } + + private fun runOnMainSync(runnable: Runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable) + } + + private fun waitForAnimateIn() { + // Advance animator for on-device test + runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) } + } + + private fun waitForAnimateOut() { + // Advance animator for on-device test + runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) } + } +} diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml index 1f3e3a4c5b22..ab1ab984fd5f 100644 --- a/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml +++ b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml @@ -1,6 +1,5 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -14,7 +13,8 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<shape android:shape="rectangle" - xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="@android:color/white" /> -</shape> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:alpha="0.35" android:color="?androidprv:attr/materialColorPrimaryContainer" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml index 468b5c2a712f..9dcde3b54421 100644 --- a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml +++ b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml @@ -1,5 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- +<?xml version="1.0" encoding="utf-8"?><!-- ~ Copyright (C) 2024 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +13,12 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<shape android:shape="rectangle" - xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="#bf309fb5" /> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> <corners android:radius="@dimen/bubble_bar_expanded_view_corner_radius" /> - <stroke android:width="1dp" android:color="#A00080FF"/> + <solid android:color="@color/bubble_drop_target_background_color" /> + <stroke + android:width="1dp" + android:color="?androidprv:attr/materialColorPrimaryContainer" /> </shape> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml index a4bbd8998cc5..147f99144b1d 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml @@ -16,13 +16,12 @@ --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@drawable/desktop_mode_resize_veil_background"> + android:layout_height="match_parent"> <ImageView android:id="@+id/veil_application_icon" - android:layout_width="96dp" - android:layout_height="96dp" + android:layout_width="@dimen/desktop_mode_resize_veil_icon_size" + android:layout_height="@dimen/desktop_mode_resize_veil_icon_size" android:layout_gravity="center" android:contentDescription="@string/app_icon_text" /> </FrameLayout>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 00fb298ea1cc..70371f6b18fc 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -213,7 +213,7 @@ <dimen name="bubble_swap_animation_offset">15dp</dimen> <!-- How far offscreen the bubble stack rests. There's some padding around the bubble so add 3dp to the desired overhang. --> - <dimen name="bubble_stack_offscreen">3dp</dimen> + <dimen name="bubble_stack_offscreen">2.5dp</dimen> <!-- How far down the screen the stack starts. --> <dimen name="bubble_stack_starting_offset_y">120dp</dimen> <!-- Space between the pointer triangle and the bubble expanded view --> @@ -506,6 +506,9 @@ <!-- The radius of the caption menu shadow. --> <dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen> + <!-- The size of the icon shown in the resize veil. --> + <dimen name="desktop_mode_resize_veil_icon_size">96dp</dimen> + <dimen name="freeform_resize_handle">15dp</dimen> <dimen name="freeform_resize_corner">44dp</dimen> @@ -535,5 +538,7 @@ <!-- The vertical margin that needs to be preserved between the scaled window bounds and the original window bounds (once the surface is scaled enough to do so) --> <dimen name="cross_task_back_vertical_margin">8dp</dimen> + <!-- The offset from the left edge of the entering page for the cross-activity animation --> + <dimen name="cross_activity_back_entering_start_offset">96dp</dimen> </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java deleted file mode 100644 index d6f7c367f772..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java +++ /dev/null @@ -1,455 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.back; - -import static android.view.RemoteAnimationTarget.MODE_CLOSING; -import static android.view.RemoteAnimationTarget.MODE_OPENING; -import static android.window.BackEvent.EDGE_RIGHT; - -import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY; -import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD; -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.NonNull; -import android.content.Context; -import android.graphics.Matrix; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.RectF; -import android.os.RemoteException; -import android.util.FloatProperty; -import android.util.TypedValue; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.animation.Interpolator; -import android.window.BackEvent; -import android.window.BackMotionEvent; -import android.window.BackProgressAnimator; -import android.window.IOnBackInvokedCallback; - -import com.android.internal.dynamicanimation.animation.SpringAnimation; -import com.android.internal.dynamicanimation.animation.SpringForce; -import com.android.internal.policy.ScreenDecorationsUtils; -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.animation.Interpolators; -import com.android.wm.shell.common.annotations.ShellMainThread; - -import javax.inject.Inject; - -/** Class that defines cross-activity animation. */ -@ShellMainThread -public class CrossActivityBackAnimation extends ShellBackAnimation { - /** - * Minimum scale of the entering/closing window. - */ - private static final float MIN_WINDOW_SCALE = 0.9f; - - /** Duration of post animation after gesture committed. */ - private static final int POST_ANIMATION_DURATION = 350; - private static final Interpolator INTERPOLATOR = Interpolators.STANDARD_DECELERATE; - private static final FloatProperty<CrossActivityBackAnimation> ENTER_PROGRESS_PROP = - new FloatProperty<>("enter-alpha") { - @Override - public void setValue(CrossActivityBackAnimation anim, float value) { - anim.setEnteringProgress(value); - } - - @Override - public Float get(CrossActivityBackAnimation object) { - return object.getEnteringProgress(); - } - }; - private static final FloatProperty<CrossActivityBackAnimation> LEAVE_PROGRESS_PROP = - new FloatProperty<>("leave-alpha") { - @Override - public void setValue(CrossActivityBackAnimation anim, float value) { - anim.setLeavingProgress(value); - } - - @Override - public Float get(CrossActivityBackAnimation object) { - return object.getLeavingProgress(); - } - }; - private static final float MIN_WINDOW_ALPHA = 0.01f; - private static final float WINDOW_X_SHIFT_DP = 48; - private static final int SCALE_FACTOR = 100; - // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists. - private static final float TARGET_COMMIT_PROGRESS = 0.5f; - private static final float ENTER_ALPHA_THRESHOLD = 0.22f; - - private final Rect mStartTaskRect = new Rect(); - private final float mCornerRadius; - - // The closing window properties. - private final RectF mClosingRect = new RectF(); - - // The entering window properties. - private final Rect mEnteringStartRect = new Rect(); - private final RectF mEnteringRect = new RectF(); - private final SpringAnimation mEnteringProgressSpring; - private final SpringAnimation mLeavingProgressSpring; - // Max window x-shift in pixels. - private final float mWindowXShift; - private final BackAnimationRunner mBackAnimationRunner; - - private float mEnteringProgress = 0f; - private float mLeavingProgress = 0f; - - private final PointF mInitialTouchPos = new PointF(); - - private final Matrix mTransformMatrix = new Matrix(); - - private final float[] mTmpFloat9 = new float[9]; - - private RemoteAnimationTarget mEnteringTarget; - private RemoteAnimationTarget mClosingTarget; - private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction(); - - private boolean mBackInProgress = false; - private boolean mIsRightEdge; - private boolean mTriggerBack = false; - - private PointF mTouchPos = new PointF(); - private IRemoteAnimationFinishedCallback mFinishCallback; - - private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); - - private final BackAnimationBackground mBackground; - - @Inject - public CrossActivityBackAnimation(Context context, BackAnimationBackground background) { - mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); - mBackAnimationRunner = new BackAnimationRunner( - new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY); - mBackground = background; - mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP); - mEnteringProgressSpring.setSpring(new SpringForce() - .setStiffness(SpringForce.STIFFNESS_MEDIUM) - .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); - mLeavingProgressSpring = new SpringAnimation(this, LEAVE_PROGRESS_PROP); - mLeavingProgressSpring.setSpring(new SpringForce() - .setStiffness(SpringForce.STIFFNESS_MEDIUM) - .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); - mWindowXShift = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WINDOW_X_SHIFT_DP, - context.getResources().getDisplayMetrics()); - } - - /** - * Returns 1 if x >= edge1, 0 if x <= edge0, and a smoothed value between the two. - * From https://en.wikipedia.org/wiki/Smoothstep - */ - private static float smoothstep(float edge0, float edge1, float x) { - if (x < edge0) return 0; - if (x >= edge1) return 1; - - x = (x - edge0) / (edge1 - edge0); - return x * x * (3 - 2 * x); - } - - /** - * Linearly map x from range (a1, a2) to range (b1, b2). - */ - private static float mapLinear(float x, float a1, float a2, float b1, float b2) { - return b1 + (x - a1) * (b2 - b1) / (a2 - a1); - } - - /** - * Linearly map a normalized value from (0, 1) to (min, max). - */ - private static float mapRange(float value, float min, float max) { - return min + (value * (max - min)); - } - - private void startBackAnimation() { - if (mEnteringTarget == null || mClosingTarget == null) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null."); - return; - } - mTransaction.setAnimationTransaction(); - - // Offset start rectangle to align task bounds. - mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds()); - mStartTaskRect.offsetTo(0, 0); - - // Draw background with task background color. - mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(), - mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction); - setEnteringProgress(0); - setLeavingProgress(0); - } - - private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) { - if (leash == null || !leash.isValid()) { - return; - } - - final float scale = targetRect.width() / mStartTaskRect.width(); - mTransformMatrix.reset(); - mTransformMatrix.setScale(scale, scale); - mTransformMatrix.postTranslate(targetRect.left, targetRect.top); - mTransaction.setAlpha(leash, targetAlpha) - .setMatrix(leash, mTransformMatrix, mTmpFloat9) - .setWindowCrop(leash, mStartTaskRect) - .setCornerRadius(leash, mCornerRadius); - } - - private void finishAnimation() { - if (mEnteringTarget != null) { - if (mEnteringTarget.leash != null && mEnteringTarget.leash.isValid()) { - mTransaction.setCornerRadius(mEnteringTarget.leash, 0); - mEnteringTarget.leash.release(); - } - mEnteringTarget = null; - } - if (mClosingTarget != null) { - if (mClosingTarget.leash != null) { - mClosingTarget.leash.release(); - } - mClosingTarget = null; - } - if (mBackground != null) { - mBackground.removeBackground(mTransaction); - } - - mTransaction.apply(); - mBackInProgress = false; - mTransformMatrix.reset(); - mInitialTouchPos.set(0, 0); - - if (mFinishCallback != null) { - try { - mFinishCallback.onAnimationFinished(); - } catch (RemoteException e) { - e.printStackTrace(); - } - mFinishCallback = null; - } - mEnteringProgressSpring.animateToFinalPosition(0); - mEnteringProgressSpring.skipToEnd(); - mLeavingProgressSpring.animateToFinalPosition(0); - mLeavingProgressSpring.skipToEnd(); - } - - private void onGestureProgress(@NonNull BackEvent backEvent) { - if (!mBackInProgress) { - mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT; - mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); - mBackInProgress = true; - } - mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY()); - - float progress = backEvent.getProgress(); - float springProgress = (mTriggerBack - ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1) - : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR; - mLeavingProgressSpring.animateToFinalPosition(springProgress); - mEnteringProgressSpring.animateToFinalPosition(springProgress); - mBackground.onBackProgressed(progress); - } - - private void onGestureCommitted() { - if (mEnteringTarget == null || mClosingTarget == null || mClosingTarget.leash == null - || mEnteringTarget.leash == null || !mEnteringTarget.leash.isValid() - || !mClosingTarget.leash.isValid()) { - finishAnimation(); - return; - } - // End the fade animations - mLeavingProgressSpring.cancel(); - mEnteringProgressSpring.cancel(); - - // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current - // coordinate of the gesture driven phase. - mEnteringRect.round(mEnteringStartRect); - mTransaction.hide(mClosingTarget.leash); - - ValueAnimator valueAnimator = - ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION); - valueAnimator.setInterpolator(INTERPOLATOR); - valueAnimator.addUpdateListener(animation -> { - float progress = animation.getAnimatedFraction(); - updatePostCommitEnteringAnimation(progress); - if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) { - mBackground.resetStatusBarCustomization(); - } - mTransaction.apply(); - }); - - valueAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mBackground.resetStatusBarCustomization(); - finishAnimation(); - } - }); - valueAnimator.start(); - } - - private void updatePostCommitEnteringAnimation(float progress) { - float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left); - float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top); - float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width()); - float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height()); - float alpha = mapRange(progress, getPreCommitEnteringAlpha(), 1.0f); - mEnteringRect.set(left, top, left + width, top + height); - applyTransform(mEnteringTarget.leash, mEnteringRect, alpha); - } - - private float getPreCommitEnteringAlpha() { - return Math.max(smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress), - MIN_WINDOW_ALPHA); - } - - private float getEnteringProgress() { - return mEnteringProgress * SCALE_FACTOR; - } - - private void setEnteringProgress(float value) { - mEnteringProgress = value / SCALE_FACTOR; - if (mEnteringTarget != null && mEnteringTarget.leash != null) { - transformWithProgress( - mEnteringProgress, - getPreCommitEnteringAlpha(), - mEnteringTarget.leash, - mEnteringRect, - -mWindowXShift, - 0 - ); - } - } - - private float getPreCommitLeavingAlpha() { - return Math.max(1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress), - MIN_WINDOW_ALPHA); - } - - private float getLeavingProgress() { - return mLeavingProgress * SCALE_FACTOR; - } - - private void setLeavingProgress(float value) { - mLeavingProgress = value / SCALE_FACTOR; - if (mClosingTarget != null && mClosingTarget.leash != null) { - transformWithProgress( - mLeavingProgress, - getPreCommitLeavingAlpha(), - mClosingTarget.leash, - mClosingRect, - 0, - mIsRightEdge ? 0 : mWindowXShift - ); - } - } - - private void transformWithProgress(float progress, float alpha, SurfaceControl surface, - RectF targetRect, float deltaXMin, float deltaXMax) { - - final int width = mStartTaskRect.width(); - final int height = mStartTaskRect.height(); - - final float interpolatedProgress = INTERPOLATOR.getInterpolation(progress); - final float closingScale = MIN_WINDOW_SCALE - + (1 - interpolatedProgress) * (1 - MIN_WINDOW_SCALE); - final float closingWidth = closingScale * width; - final float closingHeight = (float) height / width * closingWidth; - - // Move the window along the X axis. - float closingLeft = mStartTaskRect.left + (width - closingWidth) / 2; - closingLeft += mapRange(interpolatedProgress, deltaXMin, deltaXMax); - - // Move the window along the Y axis. - final float closingTop = (height - closingHeight) * 0.5f; - targetRect.set( - closingLeft, closingTop, closingLeft + closingWidth, closingTop + closingHeight); - - applyTransform(surface, targetRect, Math.max(alpha, MIN_WINDOW_ALPHA)); - mTransaction.apply(); - } - - @Override - public BackAnimationRunner getRunner() { - return mBackAnimationRunner; - } - - private final class Callback extends IOnBackInvokedCallback.Default { - @Override - public void onBackStarted(BackMotionEvent backEvent) { - mTriggerBack = backEvent.getTriggerBack(); - mProgressAnimator.onBackStarted(backEvent, - CrossActivityBackAnimation.this::onGestureProgress); - } - - @Override - public void onBackProgressed(@NonNull BackMotionEvent backEvent) { - mTriggerBack = backEvent.getTriggerBack(); - mProgressAnimator.onBackProgressed(backEvent); - } - - @Override - public void onBackCancelled() { - mProgressAnimator.onBackCancelled(() -> { - // mProgressAnimator can reach finish stage earlier than mLeavingProgressSpring, - // and if we release all animation leash first, the leavingProgressSpring won't - // able to update the animation anymore, which cause flicker. - // Here should force update the closing animation target to the final stage before - // release it. - setLeavingProgress(0); - finishAnimation(); - }); - } - - @Override - public void onBackInvoked() { - mProgressAnimator.reset(); - onGestureCommitted(); - } - } - - private final class Runner extends IRemoteAnimationRunner.Default { - @Override - public void onAnimationStart( - int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to activity animation."); - for (RemoteAnimationTarget a : apps) { - if (a.mode == MODE_CLOSING) { - mClosingTarget = a; - } - if (a.mode == MODE_OPENING) { - mEnteringTarget = a; - } - } - - startBackAnimation(); - mFinishCallback = finishedCallback; - } - - @Override - public void onAnimationCancelled() { - finishAnimation(); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt new file mode 100644 index 000000000000..edf29dd484fc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.back + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.content.res.Configuration +import android.graphics.Matrix +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.RectF +import android.os.RemoteException +import android.view.Display +import android.view.IRemoteAnimationFinishedCallback +import android.view.IRemoteAnimationRunner +import android.view.RemoteAnimationTarget +import android.view.SurfaceControl +import android.view.animation.DecelerateInterpolator +import android.view.animation.Interpolator +import android.window.BackEvent +import android.window.BackMotionEvent +import android.window.BackProgressAnimator +import android.window.IOnBackInvokedCallback +import com.android.internal.jank.Cuj +import com.android.internal.policy.ScreenDecorationsUtils +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.common.annotations.ShellMainThread +import com.android.wm.shell.protolog.ShellProtoLogGroup +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +/** Class that defines cross-activity animation. */ +@ShellMainThread +class CrossActivityBackAnimation @Inject constructor( + private val context: Context, + private val background: BackAnimationBackground, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer +) : ShellBackAnimation() { + + private val startClosingRect = RectF() + private val targetClosingRect = RectF() + private val currentClosingRect = RectF() + + private val startEnteringRect = RectF() + private val targetEnteringRect = RectF() + private val currentEnteringRect = RectF() + + private val taskBoundsRect = Rect() + + private val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) + + private val backAnimationRunner = BackAnimationRunner( + Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY + ) + private val initialTouchPos = PointF() + private val transformMatrix = Matrix() + private val tmpFloat9 = FloatArray(9) + private var enteringTarget: RemoteAnimationTarget? = null + private var closingTarget: RemoteAnimationTarget? = null + private val transaction = SurfaceControl.Transaction() + private var triggerBack = false + private var finishCallback: IRemoteAnimationFinishedCallback? = null + private val progressAnimator = BackProgressAnimator() + private val displayBoundsMargin = + context.resources.getDimension(R.dimen.cross_task_back_vertical_margin) + private val enteringStartOffset = + context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset) + + private val gestureInterpolator = Interpolators.STANDARD_DECELERATE + private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN + private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator() + + private var scrimLayer: SurfaceControl? = null + private var maxScrimAlpha: Float = 0f + + override fun getRunner() = backAnimationRunner + + private fun startBackAnimation(backMotionEvent: BackMotionEvent) { + if (enteringTarget == null || closingTarget == null) { + ProtoLog.d( + ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, + "Entering target or closing target is null." + ) + return + } + triggerBack = backMotionEvent.triggerBack + initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY) + + transaction.setAnimationTransaction() + + // Offset start rectangle to align task bounds. + taskBoundsRect.set(closingTarget!!.windowConfiguration.bounds) + taskBoundsRect.offsetTo(0, 0) + + startClosingRect.set(taskBoundsRect) + + // scale closing target into the middle for rhs and to the right for lhs + targetClosingRect.set(startClosingRect) + targetClosingRect.scaleCentered(MAX_SCALE) + if (backMotionEvent.swipeEdge != BackEvent.EDGE_RIGHT) { + targetClosingRect.offset( + startClosingRect.right - targetClosingRect.right - displayBoundsMargin, 0f + ) + } + + // the entering target starts 96dp to the left of the screen edge... + startEnteringRect.set(startClosingRect) + startEnteringRect.offset(-enteringStartOffset, 0f) + + // ...and gets scaled in sync with the closing target + targetEnteringRect.set(startEnteringRect) + targetEnteringRect.scaleCentered(MAX_SCALE) + + // Draw background with task background color. + background.ensureBackground( + closingTarget!!.windowConfiguration.bounds, + enteringTarget!!.taskInfo.taskDescription!!.backgroundColor, transaction + ) + ensureScrimLayer() + transaction.apply() + } + + private fun onGestureProgress(backEvent: BackEvent) { + val progress = gestureInterpolator.getInterpolation(backEvent.progress) + background.onBackProgressed(progress) + currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress) + val yOffset = getYOffset(currentClosingRect, backEvent.touchY) + currentClosingRect.offset(0f, yOffset) + applyTransform(closingTarget?.leash, currentClosingRect, 1f) + currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress) + currentEnteringRect.offset(0f, yOffset) + applyTransform(enteringTarget?.leash, currentEnteringRect, 1f) + transaction.apply() + } + + private fun getYOffset(centeredRect: RectF, touchY: Float): Float { + val screenHeight = taskBoundsRect.height() + // Base the window movement in the Y axis on the touch movement in the Y axis. + val rawYDelta = touchY - initialTouchPos.y + val yDirection = (if (rawYDelta < 0) -1 else 1) + // limit yDelta interpretation to 1/2 of screen height in either direction + val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f) + val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio) + // limit y-shift so surface never passes 8dp screen margin + val deltaY = yDirection * interpolatedYRatio * max( + 0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin + ) + return deltaY + } + + private fun onGestureCommitted() { + if (closingTarget?.leash == null || enteringTarget?.leash == null || + !enteringTarget!!.leash.isValid || !closingTarget!!.leash.isValid + ) { + finishAnimation() + return + } + + // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current + // coordinate of the gesture driven phase. Let's update the start and target rects and kick + // off the animator + startClosingRect.set(currentClosingRect) + startEnteringRect.set(currentEnteringRect) + targetEnteringRect.set(taskBoundsRect) + targetClosingRect.set(taskBoundsRect) + targetClosingRect.offset(currentClosingRect.left + enteringStartOffset, 0f) + + val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION) + valueAnimator.addUpdateListener { animation: ValueAnimator -> + val progress = animation.animatedFraction + onPostCommitProgress(progress) + if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) { + background.resetStatusBarCustomization() + } + } + valueAnimator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + background.resetStatusBarCustomization() + finishAnimation() + } + }) + valueAnimator.start() + } + + private fun onPostCommitProgress(linearProgress: Float) { + val closingAlpha = max(1f - linearProgress * 2, 0f) + val progress = postCommitInterpolator.getInterpolation(linearProgress) + scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) } + currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress) + applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha) + currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress) + applyTransform(enteringTarget?.leash, currentEnteringRect, 1f) + transaction.apply() + } + + private fun finishAnimation() { + enteringTarget?.let { + if (it.leash != null && it.leash.isValid) { + transaction.setCornerRadius(it.leash, 0f) + it.leash.release() + } + enteringTarget = null + } + + closingTarget?.leash?.release() + closingTarget = null + + background.removeBackground(transaction) + transaction.apply() + transformMatrix.reset() + initialTouchPos.set(0f, 0f) + try { + finishCallback?.onAnimationFinished() + } catch (e: RemoteException) { + e.printStackTrace() + } + finishCallback = null + removeScrimLayer() + } + + private fun applyTransform(leash: SurfaceControl?, rect: RectF, alpha: Float) { + if (leash == null || !leash.isValid) return + val scale = rect.width() / taskBoundsRect.width() + transformMatrix.reset() + transformMatrix.setScale(scale, scale) + transformMatrix.postTranslate(rect.left, rect.top) + transaction.setAlpha(leash, alpha) + .setMatrix(leash, transformMatrix, tmpFloat9) + .setCrop(leash, taskBoundsRect) + .setCornerRadius(leash, cornerRadius) + } + + private fun ensureScrimLayer() { + if (scrimLayer != null) return + val isDarkTheme: Boolean = isDarkMode(context) + val scrimBuilder = SurfaceControl.Builder() + .setName("Cross-Activity back animation scrim") + .setCallsite("CrossActivityBackAnimation") + .setColorLayer() + .setOpaque(false) + .setHidden(false) + + rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder) + scrimLayer = scrimBuilder.build() + val colorComponents = floatArrayOf(0f, 0f, 0f) + maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT + transaction + .setColor(scrimLayer, colorComponents) + .setAlpha(scrimLayer!!, maxScrimAlpha) + .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1) + .show(scrimLayer) + } + + private fun removeScrimLayer() { + scrimLayer?.let { + if (it.isValid) { + transaction.remove(it).apply() + } + } + scrimLayer = null + } + + + private inner class Callback : IOnBackInvokedCallback.Default() { + override fun onBackStarted(backMotionEvent: BackMotionEvent) { + startBackAnimation(backMotionEvent) + progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent -> + onGestureProgress(backEvent) + } + } + + override fun onBackProgressed(backEvent: BackMotionEvent) { + triggerBack = backEvent.triggerBack + progressAnimator.onBackProgressed(backEvent) + } + + override fun onBackCancelled() { + progressAnimator.onBackCancelled { + finishAnimation() + } + } + + override fun onBackInvoked() { + progressAnimator.reset() + onGestureCommitted() + } + } + + private inner class Runner : IRemoteAnimationRunner.Default() { + override fun onAnimationStart( + transit: Int, + apps: Array<RemoteAnimationTarget>, + wallpapers: Array<RemoteAnimationTarget>?, + nonApps: Array<RemoteAnimationTarget>?, + finishedCallback: IRemoteAnimationFinishedCallback + ) { + ProtoLog.d( + ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "Start back to activity animation." + ) + for (a in apps) { + when (a.mode) { + RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a + RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a + } + } + finishCallback = finishedCallback + } + + override fun onAnimationCancelled() { + finishAnimation() + } + } + + companion object { + /** Max scale of the entering/closing window.*/ + private const val MAX_SCALE = 0.9f + + /** Duration of post animation after gesture committed. */ + private const val POST_ANIMATION_DURATION = 300L + + private const val MAX_SCRIM_ALPHA_DARK = 0.8f + private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f + } +} + +private fun isDarkMode(context: Context): Boolean { + return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES +} + +private fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) { + require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" } + left = start.left + (target.left - start.left) * progress + top = start.top + (target.top - start.top) * progress + right = start.right + (target.right - start.right) * progress + bottom = start.bottom + (target.bottom - start.bottom) * progress +} + +private fun RectF.scaleCentered( + scale: Float, + pivotX: Float = left + width() / 2, + pivotY: Float = top + height() / 2 +) { + offset(-pivotX, -pivotY) // move pivot to origin + scale(scale) + offset(pivotX, pivotY) // Move back to the original position +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index f4a401c64a31..4d5e516f76e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -870,7 +870,7 @@ public class BubblePositioner { if (onLeft) { left = getInsets().left + padding; } else { - left = getAvailableRect().width() - width - padding; + left = getAvailableRect().right - width - padding; } int top = getExpandedViewBottomForBubbleBar() - height; out.offsetTo(left, top); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt index 55ec6cdfe007..f6b4653b8162 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt @@ -21,6 +21,10 @@ import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import android.widget.FrameLayout.LayoutParams +import androidx.annotation.VisibleForTesting +import androidx.core.animation.Animator +import androidx.core.animation.AnimatorListenerAdapter +import androidx.core.animation.ObjectAnimator import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner import com.android.wm.shell.common.bubbles.BubbleBarLocation @@ -33,6 +37,7 @@ class BubbleBarDropTargetController( ) { private var dropTargetView: View? = null + private var animator: ObjectAnimator? = null private val tempRect: Rect by lazy(LazyThreadSafetyMode.NONE) { Rect() } /** @@ -57,7 +62,8 @@ class BubbleBarDropTargetController( /** * Set the view hidden or not * - * Requires the drop target to be first shown by calling [show]. Otherwise does not do anything. + * Requires the drop target to be first shown by calling [animateIn]. Otherwise does not do + * anything. */ fun setHidden(hidden: Boolean) { val targetView = dropTargetView ?: return @@ -106,20 +112,40 @@ class BubbleBarDropTargetController( } private fun View.animateIn() { - animate().alpha(1f).setDuration(DROP_TARGET_ALPHA_IN_DURATION).start() + animator?.cancel() + animator = + ObjectAnimator.ofFloat(this, View.ALPHA, 1f) + .setDuration(DROP_TARGET_ALPHA_IN_DURATION) + .addEndAction { animator = null } + animator?.start() } private fun View.animateOut(endAction: Runnable? = null) { - animate() - .alpha(0f) - .setDuration(DROP_TARGET_ALPHA_OUT_DURATION) - .withEndAction(endAction) - .start() + animator?.cancel() + animator = + ObjectAnimator.ofFloat(this, View.ALPHA, 0f) + .setDuration(DROP_TARGET_ALPHA_OUT_DURATION) + .addEndAction { + endAction?.run() + animator = null + } + animator?.start() + } + + private fun <T : Animator> T.addEndAction(runnable: Runnable): T { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + runnable.run() + } + } + ) + return this } companion object { - private const val DROP_TARGET_ALPHA_IN_DURATION = 150L - private const val DROP_TARGET_ALPHA_OUT_DURATION = 100L - private const val DROP_TARGET_SCALE = 0.9f + @VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L + @VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L + @VisibleForTesting const val DROP_TARGET_SCALE = 0.9f } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt index 4c34971c4fb1..9e8dfb5f0c6f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt @@ -21,11 +21,9 @@ import android.content.Context import android.content.pm.LauncherApps import android.content.pm.PackageManager import android.os.UserHandle -import android.view.WindowManager import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI import com.android.internal.annotations.VisibleForTesting import com.android.wm.shell.R -import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL import com.android.wm.shell.util.KtProtoLog import java.util.Arrays @@ -37,7 +35,8 @@ class MultiInstanceHelper @JvmOverloads constructor( private val context: Context, private val packageManager: PackageManager, private val staticAppsSupportingMultiInstance: Array<String> = context.resources - .getStringArray(R.array.config_appsSupportMultiInstancesSplit)) { + .getStringArray(R.array.config_appsSupportMultiInstancesSplit), + private val supportsMultiInstanceProperty: Boolean) { /** * Returns whether a specific component desires to be launched in multiple instances. @@ -59,6 +58,11 @@ class MultiInstanceHelper @JvmOverloads constructor( } } + if (!supportsMultiInstanceProperty) { + // If not checking the multi-instance properties, then return early + return false; + } + // Check the activity property first try { val activityProp = packageManager.getProperty( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java index e4cf6d13cb1f..98dccbbe33e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -48,6 +48,7 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.view.WindowlessWindowManager; import android.view.inputmethod.ImeTracker; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.InputTransferToken; @@ -348,7 +349,7 @@ public class SystemWindows { public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration newMergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, - boolean dragResizing) {} + boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) {} @Override public void insetsControlChanged(InsetsState insetsState, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 8d489e106ae1..512211460753 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -29,6 +29,7 @@ import android.window.SystemPerformanceHinter; import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; +import com.android.window.flags.Flags; import com.android.wm.shell.ProtoLogController; import com.android.wm.shell.R; import com.android.wm.shell.RootDisplayAreaOrganizer; @@ -326,7 +327,8 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static MultiInstanceHelper provideMultiInstanceHelper(Context context) { - return new MultiInstanceHelper(context, context.getPackageManager()); + return new MultiInstanceHelper(context, context.getPackageManager(), + Flags.supportsMultiInstanceSystemUi()); } // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index 838603f80cf1..5889da12d6e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -49,7 +49,7 @@ public interface DesktopMode { /** Called when requested to go to desktop mode from the current focused app. */ - void enterDesktop(int displayId); + void moveFocusedTaskToDesktop(int displayId); /** Called when requested to go to fullscreen from the current focused desktop app. */ void moveFocusedTaskToFullscreen(int displayId); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 992e5aecdce8..1b1c96764e88 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -263,7 +263,7 @@ class DesktopTasksController( } /** Enter desktop by using the focused task in given `displayId` */ - fun enterDesktop(displayId: Int) { + fun moveFocusedTaskToDesktop(displayId: Int) { val allFocusedTasks = shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo -> taskInfo.isFocused && @@ -1166,7 +1166,7 @@ class DesktopTasksController( pendingIntentLaunchFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED ) isPendingIntentBackgroundActivityLaunchAllowedByPermission = true } @@ -1212,9 +1212,9 @@ class DesktopTasksController( } } - override fun enterDesktop(displayId: Int) { + override fun moveFocusedTaskToDesktop(displayId: Int) { mainExecutor.execute { - this@DesktopTasksController.enterDesktop(displayId) + this@DesktopTasksController.moveFocusedTaskToDesktop(displayId) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index af26e2980afe..b830a41b6671 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -15,6 +15,7 @@ import android.content.Context import android.content.Intent import android.content.Intent.FILL_IN_COMPONENT import android.graphics.Rect +import android.os.Bundle import android.os.IBinder import android.os.SystemClock import android.view.SurfaceControl @@ -124,7 +125,7 @@ class DragToDesktopTransitionHandler( options.toBundle() ) val wct = WindowContainerTransaction() - wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle()) + wct.sendPendingIntent(pendingIntent, launchHomeIntent, Bundle()) val startTransitionToken = transitions .startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java index eb82da8a8e9f..6a7d297e83e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java @@ -16,6 +16,7 @@ package com.android.wm.shell.draganddrop; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED; import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION; @@ -301,16 +302,14 @@ public class DragAndDropPolicy { position); final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true); + baseActivityOpts.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_DENIED); // TODO(b/255649902): Rework this so that SplitScreenController can always use the options // instead of a fillInIntent since it's assuming that the PendingIntent is mutable baseActivityOpts.setPendingIntentLaunchFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_MULTIPLE_TASK); final Bundle opts = baseActivityOpts.toBundle(); - // Put BAL flags to avoid activity start aborted. - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true); - opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true); - mStarter.startIntent(session.launchableIntent, session.launchableIntent.getCreatorUserHandle().getIdentifier(), null /* fillIntent */, position, opts); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 62156fc7443b..6b5bdd2299e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -64,6 +64,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private TvPipBackgroundView mPipBackgroundView; private boolean mIsReloading; + private static final int PIP_MENU_FORCE_CLOSE_DELAY_MS = 10_000; + private final Runnable mClosePipMenuRunnable = this::closeMenu; @TvPipMenuMode private int mCurrentMenuMode = MODE_NO_MENU; @@ -280,6 +282,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: closeMenu()", TAG); requestMenuMode(MODE_NO_MENU); + mMainHandler.removeCallbacks(mClosePipMenuRunnable); } @Override @@ -488,13 +491,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private void requestMenuMode(@TvPipMenuMode int menuMode) { if (isMenuOpen() == isMenuOpen(menuMode)) { + if (mMainHandler.hasCallbacks(mClosePipMenuRunnable)) { + mMainHandler.removeCallbacks(mClosePipMenuRunnable); + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + } // No need to request a focus change. We can directly switch to the new mode. switchToMenuMode(menuMode); } else { if (isMenuOpen(menuMode)) { + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); mMenuModeOnFocus = menuMode; } - // Send a request to gain window focus if the menu is open, or lose window focus // otherwise. Once the focus change happens, we will request the new mode in the // callback {@link #onPipWindowFocusChanged}. @@ -584,6 +591,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } @Override + public void onUserInteracting() { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onUserInteracting - mCurrentMenuMode=%s", TAG, getMenuModeString()); + mMainHandler.removeCallbacks(mClosePipMenuRunnable); + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + + } + @Override public void onPipMovement(int keycode) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipMovement - mCurrentMenuMode=%s", TAG, getMenuModeString()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index b259e8d584a6..4a767ef2a113 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -491,30 +491,33 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == ACTION_UP) { - if (event.getKeyCode() == KEYCODE_BACK) { mListener.onExitCurrentMenuMode(); return true; } - - if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) { - switch (event.getKeyCode()) { - case KEYCODE_DPAD_UP: - case KEYCODE_DPAD_DOWN: - case KEYCODE_DPAD_LEFT: - case KEYCODE_DPAD_RIGHT: + switch (event.getKeyCode()) { + case KEYCODE_DPAD_UP: + case KEYCODE_DPAD_DOWN: + case KEYCODE_DPAD_LEFT: + case KEYCODE_DPAD_RIGHT: + mListener.onUserInteracting(); + if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) { mListener.onPipMovement(event.getKeyCode()); return true; - case KEYCODE_ENTER: - case KEYCODE_DPAD_CENTER: + } + break; + case KEYCODE_ENTER: + case KEYCODE_DPAD_CENTER: + mListener.onUserInteracting(); + if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) { mListener.onExitCurrentMenuMode(); return true; - default: - // Dispatch key event as normal below - } + } + break; + default: + // Dispatch key event as normal below } } - return super.dispatchKeyEvent(event); } @@ -637,6 +640,11 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L interface Listener { /** + * Called when any button (that affects the menu) on current menu mode was pressed. + */ + void onUserInteracting(); + + /** * Called when a button for exiting the current menu mode was pressed. */ void onExitCurrentMenuMode(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index 1a0c011205fb..ceac40d9ba95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -23,6 +23,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import android.annotation.BinderThread; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManager.TaskDescription; import android.graphics.Paint; @@ -42,6 +43,7 @@ import android.view.SurfaceControl; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.SnapshotDrawerUtils; import android.window.StartingWindowInfo; @@ -214,7 +216,7 @@ public class TaskSnapshotWindow { public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int seqId, - boolean dragResizing) { + boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) { final TaskSnapshotWindow snapshot = mOuter.get(); if (snapshot == null) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 9130edfa9f26..74e85f8dd468 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -334,6 +334,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { boolean isDisplayRotationAnimationStarted = false; final boolean isDreamTransition = isDreamTransition(info); final boolean isOnlyTranslucent = isOnlyTranslucent(info); + final boolean isActivityLevel = isActivityLevelOnly(info); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); @@ -502,8 +503,35 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { : new Rect(change.getEndAbsBounds()); clipRect.offsetTo(0, 0); + final TransitionInfo.Root animRoot = TransitionUtil.getRootFor(change, info); + final Point animRelOffset = new Point( + change.getEndAbsBounds().left - animRoot.getOffset().x, + change.getEndAbsBounds().top - animRoot.getOffset().y); + if (change.getActivityComponent() != null && !isActivityLevel) { + // At this point, this is an independent activity change in a non-activity + // transition. This means that an activity transition got erroneously combined + // with another ongoing transition. This then means that the animation root may + // not tightly fit the activities, so we have to put them in a separate crop. + final int layer = Transitions.calculateAnimLayer(change, i, + info.getChanges().size(), info.getType()); + final SurfaceControl leash = new SurfaceControl.Builder() + .setName("Transition ActivityWrap: " + + change.getActivityComponent().toShortString()) + .setParent(animRoot.getLeash()) + .setContainerLayer().build(); + startTransaction.setCrop(leash, clipRect); + startTransaction.setPosition(leash, animRelOffset.x, animRelOffset.y); + startTransaction.setLayer(leash, layer); + startTransaction.show(leash); + startTransaction.reparent(change.getLeash(), leash); + startTransaction.setPosition(change.getLeash(), 0, 0); + animRelOffset.set(0, 0); + finishTransaction.reparent(leash, null); + leash.release(); + } + buildSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, - mTransactionPool, mMainExecutor, change.getEndRelOffset(), cornerRadius, + mTransactionPool, mMainExecutor, animRelOffset, cornerRadius, clipRect); if (info.getAnimationOptions() != null) { @@ -612,6 +640,18 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return (translucentOpen + translucentClose) > 0; } + /** + * Does `info` only contain activity-level changes? This kinda assumes that if so, they are + * all in one task. + */ + private static boolean isActivityLevelOnly(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getActivityComponent() == null) return false; + } + return true; + } + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index ccd0b2df8cf1..a77602b3d2d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -31,7 +31,6 @@ import static android.view.WindowManager.fixScale; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; -import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; @@ -496,6 +495,7 @@ public class Transitions implements RemoteCallable<Transitions>, if (mode == TRANSIT_TO_FRONT) { // When the window is moved to front, make sure the crop is updated to prevent it // from using the old crop. + t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y); t.setWindowCrop(leash, change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); } @@ -507,6 +507,8 @@ public class Transitions implements RemoteCallable<Transitions>, t.setMatrix(leash, 1, 0, 0, 1); t.setAlpha(leash, 1.f); t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y); + t.setWindowCrop(leash, change.getEndAbsBounds().width(), + change.getEndAbsBounds().height()); } continue; } @@ -530,6 +532,44 @@ public class Transitions implements RemoteCallable<Transitions>, } } + static int calculateAnimLayer(@NonNull TransitionInfo.Change change, int i, + int numChanges, @WindowManager.TransitionType int transitType) { + // Put animating stuff above this line and put static stuff below it. + final int zSplitLine = numChanges + 1; + final boolean isOpening = isOpeningType(transitType); + final boolean isClosing = isClosingType(transitType); + final int mode = change.getMode(); + // Put all the OPEN/SHOW on top + if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { + if (isOpening + // This is for when an activity launches while a different transition is + // collecting. + || change.hasFlags(FLAG_MOVED_TO_TOP)) { + // put on top + return zSplitLine + numChanges - i; + } else { + // put on bottom + return zSplitLine - i; + } + } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { + if (isOpening) { + // put on bottom and leave visible + return zSplitLine - i; + } else { + // put on top + return zSplitLine + numChanges - i; + } + } else { // CHANGE or other + if (isClosing || TransitionUtil.isOrderOnly(change)) { + // Put below CLOSE mode (in the "static" section). + return zSplitLine - i; + } else { + // Put above CLOSE mode. + return zSplitLine + numChanges - i; + } + } + } + /** * Reparents all participants into a shared parent and orders them based on: the global transit * type, their transit mode, and their destination z-order. @@ -537,19 +577,14 @@ public class Transitions implements RemoteCallable<Transitions>, private static void setupAnimHierarchy(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { final int type = info.getType(); - final boolean isOpening = isOpeningType(type); - final boolean isClosing = isClosingType(type); for (int i = 0; i < info.getRootCount(); ++i) { t.show(info.getRoot(i).getLeash()); } final int numChanges = info.getChanges().size(); - // Put animating stuff above this line and put static stuff below it. - final int zSplitLine = numChanges + 1; // changes should be ordered top-to-bottom in z for (int i = numChanges - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); final SurfaceControl leash = change.getLeash(); - final int mode = change.getMode(); // Don't reparent anything that isn't independent within its parents if (!TransitionInfo.isIndependent(change, info)) { @@ -558,50 +593,14 @@ public class Transitions implements RemoteCallable<Transitions>, boolean hasParent = change.getParent() != null; - final int rootIdx = TransitionUtil.rootIndexFor(change, info); + final TransitionInfo.Root root = TransitionUtil.getRootFor(change, info); if (!hasParent) { - t.reparent(leash, info.getRoot(rootIdx).getLeash()); + t.reparent(leash, root.getLeash()); t.setPosition(leash, - change.getStartAbsBounds().left - info.getRoot(rootIdx).getOffset().x, - change.getStartAbsBounds().top - info.getRoot(rootIdx).getOffset().y); - } - final int layer; - // Put all the OPEN/SHOW on top - if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { - // Wallpaper is always at the bottom, opening wallpaper on top of closing one. - if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { - layer = -zSplitLine + numChanges - i; - } else { - layer = -zSplitLine - i; - } - } else if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { - if (isOpening - // This is for when an activity launches while a different transition is - // collecting. - || change.hasFlags(FLAG_MOVED_TO_TOP)) { - // put on top - layer = zSplitLine + numChanges - i; - } else { - // put on bottom - layer = zSplitLine - i; - } - } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { - if (isOpening) { - // put on bottom and leave visible - layer = zSplitLine - i; - } else { - // put on top - layer = zSplitLine + numChanges - i; - } - } else { // CHANGE or other - if (isClosing || TransitionUtil.isOrderOnly(change)) { - // Put below CLOSE mode (in the "static" section). - layer = zSplitLine - i; - } else { - // Put above CLOSE mode. - layer = zSplitLine + numChanges - i; - } + change.getStartAbsBounds().left - root.getOffset().x, + change.getStartAbsBounds().top - root.getOffset().y); } + final int layer = calculateAnimLayer(change, i, numChanges, type); t.setLayer(leash, layer); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 4c9e17155625..ad290c6aeaa3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -451,7 +451,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * until a resize event calls showResizeVeil below. */ void createResizeVeil() { - mResizeVeil = new ResizeVeil(mContext, mAppIconDrawable, mTaskInfo, + mResizeVeil = new ResizeVeil(mContext, mAppIconDrawable, mTaskInfo, mTaskSurface, mSurfaceControlBuilderSupplier, mDisplay, mSurfaceControlTransactionSupplier); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java index 6f8b3d5aaaad..76096b0c59f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor; import static android.view.WindowManager.TRANSIT_CHANGE; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.IBinder; @@ -178,10 +179,11 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, for (TransitionInfo.Change change: info.getChanges()) { final SurfaceControl sc = change.getLeash(); final Rect endBounds = change.getEndAbsBounds(); + final Point endPosition = change.getEndRelOffset(); startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); } startTransaction.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java index b0d3b5090ef0..d072f8cec194 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java @@ -23,13 +23,16 @@ import android.annotation.ColorRes; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Color; import android.graphics.PixelFormat; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.view.Display; import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.SurfaceControlViewHost; +import android.view.SurfaceSession; import android.view.View; import android.view.WindowManager; import android.view.WindowlessWindowManager; @@ -37,6 +40,7 @@ import android.widget.ImageView; import android.window.TaskConstants; import com.android.wm.shell.R; +import com.android.wm.shell.common.SurfaceUtils; import java.util.function.Supplier; @@ -45,19 +49,36 @@ import java.util.function.Supplier; */ public class ResizeVeil { private static final int RESIZE_ALPHA_DURATION = 100; + + private static final int VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL; + /** The background is a child of the veil container layer and goes at the bottom. */ + private static final int VEIL_BACKGROUND_LAYER = 0; + /** The icon is a child of the veil container layer and goes in front of the background. */ + private static final int VEIL_ICON_LAYER = 1; + private final Context mContext; private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; + private final SurfaceSession mSurfaceSession = new SurfaceSession(); private final Drawable mAppIcon; private ImageView mIconView; + private int mIconSize; private SurfaceControl mParentSurface; + + /** A container surface to host the veil background and icon child surfaces. */ private SurfaceControl mVeilSurface; + /** A color surface for the veil background. */ + private SurfaceControl mBackgroundSurface; + /** A surface that hosts a windowless window with the app icon. */ + private SurfaceControl mIconSurface; + private final RunningTaskInfo mTaskInfo; private SurfaceControlViewHost mViewHost; private final Display mDisplay; private ValueAnimator mVeilAnimator; public ResizeVeil(Context context, Drawable appIcon, RunningTaskInfo taskInfo, + SurfaceControl taskSurface, Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Display display, Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) { mContext = context; @@ -65,6 +86,7 @@ public class ResizeVeil { mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier; mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mTaskInfo = taskInfo; + mParentSurface = taskSurface; mDisplay = display; setupResizeVeil(); } @@ -73,34 +95,44 @@ public class ResizeVeil { * Create the veil in its default invisible state. */ private void setupResizeVeil() { - SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get(); - mVeilSurface = builder - .setName("Resize veil of Task= " + mTaskInfo.taskId) + mVeilSurface = mSurfaceControlBuilderSupplier.get() + .setContainerLayer() + .setName("Resize veil of Task=" + mTaskInfo.taskId) + .setHidden(true) + .setParent(mParentSurface) + .setCallsite("ResizeVeil#setupResizeVeil") + .build(); + mBackgroundSurface = SurfaceUtils.makeColorLayer(mVeilSurface, + "Resize veil background of Task=" + mTaskInfo.taskId, mSurfaceSession); + mIconSurface = mSurfaceControlBuilderSupplier.get() + .setName("Resize veil icon of Task= " + mTaskInfo.taskId) .setContainerLayer() + .setParent(mVeilSurface) + .setHidden(true) + .setCallsite("ResizeVeil#setupResizeVeil") .build(); - View v = LayoutInflater.from(mContext) - .inflate(R.layout.desktop_mode_resize_veil, null); - t.setPosition(mVeilSurface, 0, 0) - .setLayer(mVeilSurface, TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL) - .apply(); - Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); + mIconSize = mContext.getResources() + .getDimensionPixelSize(R.dimen.desktop_mode_resize_veil_icon_size); + final View root = LayoutInflater.from(mContext) + .inflate(R.layout.desktop_mode_resize_veil, null /* root */); + mIconView = root.findViewById(R.id.veil_application_icon); + mIconView.setImageDrawable(mAppIcon); + final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(taskBounds.width(), - taskBounds.height(), + new WindowManager.LayoutParams( + mIconSize, + mIconSize, WindowManager.LayoutParams.TYPE_APPLICATION, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); - lp.setTitle("Resize veil of Task=" + mTaskInfo.taskId); + lp.setTitle("Resize veil icon window of Task=" + mTaskInfo.taskId); lp.setTrustedOverlay(); - WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration, - mVeilSurface, null /* hostInputToken */); - mViewHost = new SurfaceControlViewHost(mContext, mDisplay, windowManager, "ResizeVeil"); - mViewHost.setView(v, lp); - mIconView = mViewHost.getView().findViewById(R.id.veil_application_icon); - mIconView.setImageDrawable(mAppIcon); + final WindowlessWindowManager wwm = new WindowlessWindowManager(mTaskInfo.configuration, + mIconSurface, null /* hostInputToken */); + mViewHost = new SurfaceControlViewHost(mContext, mDisplay, wwm, "ResizeVeil"); + mViewHost.setView(root, lp); } /** @@ -120,46 +152,74 @@ public class ResizeVeil { mParentSurface = parentSurface; } - int backgroundColorId = getBackgroundColorId(); - mViewHost.getView().setBackgroundColor(mContext.getColor(backgroundColorId)); + t.show(mVeilSurface); + t.setLayer(mVeilSurface, VEIL_CONTAINER_LAYER); + t.setLayer(mIconSurface, VEIL_ICON_LAYER); + t.setLayer(mBackgroundSurface, VEIL_BACKGROUND_LAYER); + t.setColor(mBackgroundSurface, + Color.valueOf(mContext.getColor(getBackgroundColorId())).getComponents()); relayout(taskBounds, t); if (fadeIn) { cancelAnimation(); + final SurfaceControl.Transaction veilAnimT = mSurfaceControlTransactionSupplier.get(); mVeilAnimator = new ValueAnimator(); mVeilAnimator.setFloatValues(0f, 1f); mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); mVeilAnimator.addUpdateListener(animation -> { - t.setAlpha(mVeilSurface, mVeilAnimator.getAnimatedFraction()); - t.apply(); + veilAnimT.setAlpha(mBackgroundSurface, mVeilAnimator.getAnimatedFraction()); + veilAnimT.apply(); }); mVeilAnimator.addListener(new AnimatorListenerAdapter() { @Override + public void onAnimationStart(Animator animation) { + veilAnimT.show(mBackgroundSurface) + .setAlpha(mBackgroundSurface, 0) + .apply(); + } + + @Override public void onAnimationEnd(Animator animation) { - t.setAlpha(mVeilSurface, 1); - t.apply(); + veilAnimT.setAlpha(mBackgroundSurface, 1).apply(); } }); + final SurfaceControl.Transaction iconAnimT = mSurfaceControlTransactionSupplier.get(); final ValueAnimator iconAnimator = new ValueAnimator(); iconAnimator.setFloatValues(0f, 1f); iconAnimator.setDuration(RESIZE_ALPHA_DURATION); iconAnimator.addUpdateListener(animation -> { - mIconView.setAlpha(animation.getAnimatedFraction()); + iconAnimT.setAlpha(mIconSurface, animation.getAnimatedFraction()); + iconAnimT.apply(); }); + iconAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + iconAnimT.show(mIconSurface) + .setAlpha(mIconSurface, 0) + .apply(); + } + + @Override + public void onAnimationEnd(Animator animation) { + iconAnimT.setAlpha(mIconSurface, 1).apply(); + } + }); + // Let the animators show it with the correct alpha value once the animation starts. + t.hide(mIconSurface); + t.hide(mBackgroundSurface); + t.apply(); - t.show(mVeilSurface) - .addTransactionCommittedListener( - mContext.getMainExecutor(), () -> { - mVeilAnimator.start(); - iconAnimator.start(); - }) - .setAlpha(mVeilSurface, 0); + mVeilAnimator.start(); + iconAnimator.start(); } else { - // Show the veil immediately at full opacity. - t.show(mVeilSurface).setAlpha(mVeilSurface, 1); + // Show the veil immediately. + t.show(mIconSurface); + t.show(mBackgroundSurface); + t.setAlpha(mIconSurface, 1); + t.setAlpha(mBackgroundSurface, 1); + t.apply(); } - mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); } /** @@ -175,8 +235,9 @@ public class ResizeVeil { * @param newBounds bounds to update veil to. */ private void relayout(Rect newBounds, SurfaceControl.Transaction t) { - mViewHost.relayout(newBounds.width(), newBounds.height()); t.setWindowCrop(mVeilSurface, newBounds.width(), newBounds.height()); + final PointF iconPosition = calculateAppIconPosition(newBounds); + t.setPosition(mIconSurface, iconPosition.x, iconPosition.y); t.setPosition(mParentSurface, newBounds.left, newBounds.top); t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height()); } @@ -204,7 +265,7 @@ public class ResizeVeil { mVeilAnimator.end(); } relayout(newBounds, t); - mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t); + t.apply(); } /** @@ -217,14 +278,16 @@ public class ResizeVeil { mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); mVeilAnimator.addUpdateListener(animation -> { SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.setAlpha(mVeilSurface, 1 - mVeilAnimator.getAnimatedFraction()); + t.setAlpha(mBackgroundSurface, 1 - mVeilAnimator.getAnimatedFraction()); + t.setAlpha(mIconSurface, 1 - mVeilAnimator.getAnimatedFraction()); t.apply(); }); mVeilAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.hide(mVeilSurface); + t.hide(mBackgroundSurface); + t.hide(mIconSurface); t.apply(); } }); @@ -242,6 +305,11 @@ public class ResizeVeil { } } + private PointF calculateAppIconPosition(Rect parentBounds) { + return new PointF((float) parentBounds.width() / 2 - (float) mIconSize / 2, + (float) parentBounds.height() / 2 - (float) mIconSize / 2); + } + private void cancelAnimation() { if (mVeilAnimator != null) { mVeilAnimator.removeAllUpdateListeners(); @@ -260,11 +328,19 @@ public class ResizeVeil { mViewHost.release(); mViewHost = null; } + final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); + if (mBackgroundSurface != null) { + t.remove(mBackgroundSurface); + mBackgroundSurface = null; + } + if (mIconSurface != null) { + t.remove(mIconSurface); + mIconSurface = null; + } if (mVeilSurface != null) { - final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); t.remove(mVeilSurface); mVeilSurface = null; - t.apply(); } + t.apply(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index c12a93edcaf3..5fce5d228d71 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor; import static android.view.WindowManager.TRANSIT_CHANGE; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.IBinder; @@ -179,10 +180,11 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, for (TransitionInfo.Change change: info.getChanges()) { final SurfaceControl sc = change.getLeash(); final Rect endBounds = change.getEndAbsBounds(); + final Point endPosition = change.getEndRelOffset(); startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); } startTransaction.apply(); diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt index 1ccc7d8084a6..5f25d70acf7c 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt @@ -24,6 +24,7 @@ import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.WindowUtils import android.tools.traces.parsers.toFlickerComponent +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions @@ -181,6 +182,12 @@ class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) : } } + /** {@inheritDoc} */ + @FlakyTest(bugId = 312446524) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 9ded6ea1d187..703eb199f260 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -61,6 +61,7 @@ import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; import com.android.internal.util.test.FakeSettingsProvider; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -113,6 +114,8 @@ public class BackAnimationControllerTest extends ShellTestCase { private InputManager mInputManager; @Mock private ShellCommandHandler mShellCommandHandler; + @Mock + private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; private BackAnimationController mController; private TestableContentResolver mContentResolver; @@ -133,7 +136,8 @@ public class BackAnimationControllerTest extends ShellTestCase { mShellInit = spy(new ShellInit(mShellExecutor)); mShellBackAnimationRegistry = new ShellBackAnimationRegistry( - new CrossActivityBackAnimation(mContext, mAnimationBackground), + new CrossActivityBackAnimation( + mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer), new CrossTaskBackAnimation(mContext, mAnimationBackground), /* dialogCloseAnimation= */ null, new CustomizeActivityAnimation(mContext, mAnimationBackground), @@ -528,8 +532,8 @@ public class BackAnimationControllerTest extends ShellTestCase { @Test public void testBackToActivity() throws RemoteException { - final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(mContext, - mAnimationBackground); + final CrossActivityBackAnimation animation = new CrossActivityBackAnimation( + mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer); verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_ACTIVITY, animation.getRunner()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt index 2f5fe11634a4..bec91e910cf7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt @@ -32,9 +32,12 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.eq +import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @@ -77,7 +80,7 @@ class MultiInstanceHelperTest : ShellTestCase() { @Test fun supportsMultiInstanceSplit_inStaticAllowList() { val allowList = arrayOf(TEST_PACKAGE) - val helper = MultiInstanceHelper(mContext, context.packageManager, allowList) + val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true) val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) assertEquals(true, helper.supportsMultiInstanceSplit(component)) } @@ -85,7 +88,7 @@ class MultiInstanceHelperTest : ShellTestCase() { @Test fun supportsMultiInstanceSplit_notInStaticAllowList() { val allowList = arrayOf(TEST_PACKAGE) - val helper = MultiInstanceHelper(mContext, context.packageManager, allowList) + val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true) val component = ComponentName(TEST_NOT_ALLOWED_PACKAGE, TEST_ACTIVITY) assertEquals(false, helper.supportsMultiInstanceSplit(component)) } @@ -104,7 +107,7 @@ class MultiInstanceHelperTest : ShellTestCase() { eq(component.packageName))) .thenReturn(appProp) - val helper = MultiInstanceHelper(mContext, pm, emptyArray()) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) // Expect activity property to override application property assertEquals(true, helper.supportsMultiInstanceSplit(component)) } @@ -123,7 +126,7 @@ class MultiInstanceHelperTest : ShellTestCase() { eq(component.packageName))) .thenReturn(appProp) - val helper = MultiInstanceHelper(mContext, pm, emptyArray()) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) // Expect activity property to override application property assertEquals(false, helper.supportsMultiInstanceSplit(component)) } @@ -141,7 +144,7 @@ class MultiInstanceHelperTest : ShellTestCase() { eq(component.packageName))) .thenReturn(appProp) - val helper = MultiInstanceHelper(mContext, pm, emptyArray()) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) // Expect fall through to app property assertEquals(true, helper.supportsMultiInstanceSplit(component)) } @@ -158,10 +161,30 @@ class MultiInstanceHelperTest : ShellTestCase() { eq(component.packageName))) .thenThrow(PackageManager.NameNotFoundException()) - val helper = MultiInstanceHelper(mContext, pm, emptyArray()) + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true) assertEquals(false, helper.supportsMultiInstanceSplit(component)) } + @Test + @Throws(PackageManager.NameNotFoundException::class) + fun checkNoMultiInstancePropertyFlag_ignoreProperty() { + val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY) + val pm = mock<PackageManager>() + val activityProp = PackageManager.Property("", true, "", "") + whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component))) + .thenReturn(activityProp) + val appProp = PackageManager.Property("", true, "", "") + whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI), + eq(component.packageName))) + .thenReturn(appProp) + + val helper = MultiInstanceHelper(mContext, pm, emptyArray(), false) + // Expect we only check the static list and not the property + assertEquals(false, helper.supportsMultiInstanceSplit(component)) + verify(pm, never()).getProperty(any(), any<ComponentName>()) + } + companion object { val TEST_PACKAGE = "com.android.wm.shell.common" val TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.common.fake"; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 254bf7da08a6..4fbf2bddb7b2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -833,7 +833,7 @@ class DesktopTasksControllerTest : ShellTestCase() { verify(launchAdjacentController).launchAdjacentEnabled = true } @Test - fun enterDesktop_fullscreenTaskIsMovedToDesktop() { + fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() { val task1 = setUpFullscreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -842,7 +842,7 @@ class DesktopTasksControllerTest : ShellTestCase() { task2.isFocused = false task3.isFocused = false - controller.enterDesktop(DEFAULT_DISPLAY) + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task1.token.asBinder()]?.windowingMode) @@ -850,7 +850,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun enterDesktop_splitScreenTaskIsMovedToDesktop() { + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { val task1 = setUpSplitScreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -863,7 +863,7 @@ class DesktopTasksControllerTest : ShellTestCase() { task4.parentTaskId = task1.taskId - controller.enterDesktop(DEFAULT_DISPLAY) + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task4.token.asBinder()]?.windowingMode) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt index ce7b63322b4a..9174556d091b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt @@ -2,6 +2,7 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.graphics.Point import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner @@ -11,6 +12,7 @@ import android.view.Surface.ROTATION_270 import android.view.Surface.ROTATION_90 import android.view.SurfaceControl import android.view.WindowManager +import android.window.TransitionInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING @@ -41,6 +43,8 @@ import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.doReturn import java.util.function.Supplier +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock import org.mockito.Mockito.`when` as whenever /** @@ -575,6 +579,32 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { }) } + @Test + fun testStartAnimation_useEndRelOffset() { + val mockTransitionInfo = mock(TransitionInfo::class.java) + val changeMock = mock(TransitionInfo.Change::class.java) + val startTransaction = mock(SurfaceControl.Transaction::class.java) + val finishTransaction = mock(SurfaceControl.Transaction::class.java) + val point = Point(10, 20) + val bounds = Rect(1, 2, 3, 4) + `when`(changeMock.endRelOffset).thenReturn(point) + `when`(changeMock.endAbsBounds).thenReturn(bounds) + `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock)) + `when`(startTransaction.setWindowCrop(any(), + eq(bounds.width()), + eq(bounds.height()))).thenReturn(startTransaction) + `when`(finishTransaction.setWindowCrop(any(), + eq(bounds.width()), + eq(bounds.height()))).thenReturn(finishTransaction) + + taskPositioner.startAnimation(mockTransitionBinder, mockTransitionInfo, startTransaction, + finishTransaction, { _ -> }) + + verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(changeMock).endRelOffset + } + private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean { return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) && bounds == configuration.windowConfiguration.bounds diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index 7f6e538f0bbf..a9f44929fc64 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.graphics.Point import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner @@ -25,6 +26,7 @@ import android.view.Surface.ROTATION_0 import android.view.Surface.ROTATION_270 import android.view.Surface.ROTATION_90 import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CHANGE import android.window.TransitionInfo import android.window.WindowContainerToken @@ -39,6 +41,7 @@ import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED +import java.util.function.Supplier import junit.framework.Assert import org.junit.Before import org.junit.Test @@ -47,13 +50,13 @@ import org.mockito.Mock import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.eq +import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations -import java.util.function.Supplier import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations /** * Tests for [VeiledResizeTaskPositioner]. @@ -439,6 +442,40 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { Assert.assertFalse(taskPositioner.isResizingOrAnimating) } + @Test + fun testStartAnimation_useEndRelOffset() { + val changeMock = mock(TransitionInfo.Change::class.java) + val startTransaction = mock(Transaction::class.java) + val finishTransaction = mock(Transaction::class.java) + val point = Point(10, 20) + val bounds = Rect(1, 2, 3, 4) + `when`(changeMock.endRelOffset).thenReturn(point) + `when`(changeMock.endAbsBounds).thenReturn(bounds) + `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock)) + `when`(startTransaction.setWindowCrop( + any(), + eq(bounds.width()), + eq(bounds.height()) + )).thenReturn(startTransaction) + `when`(finishTransaction.setWindowCrop( + any(), + eq(bounds.width()), + eq(bounds.height()) + )).thenReturn(finishTransaction) + + taskPositioner.startAnimation( + mockTransitionBinder, + mockTransitionInfo, + startTransaction, + finishTransaction, + mockFinishCallback + ) + + verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(changeMock).endRelOffset + } + private fun performDrag( startX: Float, startY: Float, diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 76a0a6499d33..659bcdc6852d 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -2,6 +2,7 @@ package: "com.android.graphics.hwui.flags" flag { name: "clip_shader" + is_exported: true namespace: "core_graphics" description: "API for canvas shader clipping operations" bug: "280116960" @@ -9,6 +10,7 @@ flag { flag { name: "matrix_44" + is_exported: true namespace: "core_graphics" description: "API for 4x4 matrix and related canvas functions" bug: "280116960" @@ -16,6 +18,7 @@ flag { flag { name: "limited_hdr" + is_exported: true namespace: "core_graphics" description: "API to enable apps to restrict the amount of HDR headroom that is used" bug: "234181960" @@ -44,6 +47,7 @@ flag { flag { name: "gainmap_animations" + is_exported: true namespace: "core_graphics" description: "APIs to help enable animations involving gainmaps" bug: "296482289" @@ -51,6 +55,7 @@ flag { flag { name: "gainmap_constructor_with_metadata" + is_exported: true namespace: "core_graphics" description: "APIs to create a new gainmap with a bitmap for metadata." bug: "304478551" @@ -65,6 +70,7 @@ flag { flag { name: "requested_formats_v" + is_exported: true namespace: "core_graphics" description: "Enable r_8, r_16_uint, rg_1616_uint, and rgba_10101010 in the SDK" bug: "292545615" diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig index f33bcb7f9643..19e59a776511 100644 --- a/location/java/android/location/flags/location.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -8,7 +8,15 @@ flag { } flag { + name: "enable_location_bypass" + namespace: "location" + description: "Enable location bypass feature" + bug: "301150056" +} + +flag { name: "location_bypass" + is_exported: true namespace: "location" description: "Enable location bypass appops behavior" bug: "329151785" diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index ab7c27f70e05..2d7db5e6ed94 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -35,6 +35,7 @@ import android.media.tv.tunerresourcemanager.TunerResourceManager; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; +import android.os.IBinder; import android.os.IHwBinder; import android.os.Looper; import android.os.Message; @@ -43,7 +44,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; import android.util.Log; -import android.util.Singleton; import com.android.internal.util.FrameworkStatsLog; @@ -264,71 +264,107 @@ public final class MediaCas implements AutoCloseable { public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = android.hardware.cas.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED; - private static final Singleton<IMediaCasService> sService = - new Singleton<IMediaCasService>() { + private static IMediaCasService sService = null; + private static Object sAidlLock = new Object(); + + /** DeathListener for AIDL service */ + private static IBinder.DeathRecipient sDeathListener = + new IBinder.DeathRecipient() { @Override - protected IMediaCasService create() { - try { - Log.d(TAG, "Trying to get AIDL service"); - IMediaCasService serviceAidl = - IMediaCasService.Stub.asInterface( - ServiceManager.waitForDeclaredService( - IMediaCasService.DESCRIPTOR + "/default")); - if (serviceAidl != null) { - return serviceAidl; - } - } catch (Exception eAidl) { - Log.d(TAG, "Failed to get cas AIDL service"); + public void binderDied() { + synchronized (sAidlLock) { + Log.d(TAG, "The service is dead"); + sService.asBinder().unlinkToDeath(sDeathListener, 0); + sService = null; } - return null; } }; - private static final Singleton<android.hardware.cas.V1_0.IMediaCasService> sServiceHidl = - new Singleton<android.hardware.cas.V1_0.IMediaCasService>() { - @Override - protected android.hardware.cas.V1_0.IMediaCasService create() { - try { - Log.d(TAG, "Trying to get cas@1.2 service"); - android.hardware.cas.V1_2.IMediaCasService serviceV12 = - android.hardware.cas.V1_2.IMediaCasService.getService( - true /*wait*/); - if (serviceV12 != null) { - return serviceV12; - } - } catch (Exception eV1_2) { - Log.d(TAG, "Failed to get cas@1.2 service"); + static IMediaCasService getService() { + synchronized (sAidlLock) { + if (sService == null || !sService.asBinder().isBinderAlive()) { + try { + Log.d(TAG, "Trying to get AIDL service"); + sService = + IMediaCasService.Stub.asInterface( + ServiceManager.waitForDeclaredService( + IMediaCasService.DESCRIPTOR + "/default")); + if (sService != null) { + sService.asBinder().linkToDeath(sDeathListener, 0); } + } catch (Exception eAidl) { + Log.d(TAG, "Failed to get cas AIDL service"); + } + } + return sService; + } + } - try { - Log.d(TAG, "Trying to get cas@1.1 service"); - android.hardware.cas.V1_1.IMediaCasService serviceV11 = - android.hardware.cas.V1_1.IMediaCasService.getService( - true /*wait*/); - if (serviceV11 != null) { - return serviceV11; + private static android.hardware.cas.V1_0.IMediaCasService sServiceHidl = null; + private static Object sHidlLock = new Object(); + + /** Used to indicate the right end-point to handle the serviceDied method */ + private static final long MEDIA_CAS_HIDL_COOKIE = 394; + + /** DeathListener for HIDL service */ + private static IHwBinder.DeathRecipient sDeathListenerHidl = + new IHwBinder.DeathRecipient() { + @Override + public void serviceDied(long cookie) { + if (cookie == MEDIA_CAS_HIDL_COOKIE) { + synchronized (sHidlLock) { + sServiceHidl = null; } - } catch (Exception eV1_1) { - Log.d(TAG, "Failed to get cas@1.1 service"); } + } + }; - try { - Log.d(TAG, "Trying to get cas@1.0 service"); - return android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/); - } catch (Exception eV1_0) { - Log.d(TAG, "Failed to get cas@1.0 service"); + static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() { + synchronized (sHidlLock) { + if (sServiceHidl != null) { + return sServiceHidl; + } else { + try { + Log.d(TAG, "Trying to get cas@1.2 service"); + android.hardware.cas.V1_2.IMediaCasService serviceV12 = + android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/); + if (serviceV12 != null) { + sServiceHidl = serviceV12; + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + return sServiceHidl; } - - return null; + } catch (Exception eV1_2) { + Log.d(TAG, "Failed to get cas@1.2 service"); } - }; - static IMediaCasService getService() { - return sService.get(); - } + try { + Log.d(TAG, "Trying to get cas@1.1 service"); + android.hardware.cas.V1_1.IMediaCasService serviceV11 = + android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/); + if (serviceV11 != null) { + sServiceHidl = serviceV11; + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + return sServiceHidl; + } + } catch (Exception eV1_1) { + Log.d(TAG, "Failed to get cas@1.1 service"); + } - static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() { - return sServiceHidl.get(); + try { + Log.d(TAG, "Trying to get cas@1.0 service"); + sServiceHidl = + android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/); + if (sServiceHidl != null) { + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + } + return sServiceHidl; + } catch (Exception eV1_0) { + Log.d(TAG, "Failed to get cas@1.0 service"); + } + } + } + // Couldn't find an HIDL service, returning null. + return null; } private void validateInternalStates() { @@ -756,7 +792,7 @@ public final class MediaCas implements AutoCloseable { * @return Whether the specified CA system is supported on this device. */ public static boolean isSystemIdSupported(int CA_system_id) { - IMediaCasService service = sService.get(); + IMediaCasService service = getService(); if (service != null) { try { return service.isSystemIdSupported(CA_system_id); @@ -765,7 +801,7 @@ public final class MediaCas implements AutoCloseable { } } - android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl(); if (serviceHidl != null) { try { return serviceHidl.isSystemIdSupported(CA_system_id); @@ -781,7 +817,7 @@ public final class MediaCas implements AutoCloseable { * @return an array of descriptors for the available CA plugins. */ public static PluginDescriptor[] enumeratePlugins() { - IMediaCasService service = sService.get(); + IMediaCasService service = getService(); if (service != null) { try { AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins(); @@ -794,10 +830,11 @@ public final class MediaCas implements AutoCloseable { } return results; } catch (RemoteException e) { + Log.e(TAG, "Some exception while enumerating plugins"); } } - android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl(); if (serviceHidl != null) { try { ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.enumeratePlugins(); diff --git a/media/java/android/media/flags/editing.aconfig b/media/java/android/media/flags/editing.aconfig index c3997e94622d..5bf1b4ef96ff 100644 --- a/media/java/android/media/flags/editing.aconfig +++ b/media/java/android/media/flags/editing.aconfig @@ -2,6 +2,7 @@ package: "com.android.media.editing.flags" flag { name: "add_media_metrics_editing" + is_exported: true namespace: "media_solutions" description: "Add media metrics for transcoding/editing events." bug: "297487694" diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig index bf3942559b8a..40929f79eeb6 100644 --- a/media/java/android/media/flags/media_better_together.aconfig +++ b/media/java/android/media/flags/media_better_together.aconfig @@ -2,6 +2,7 @@ package: "com.android.media.flags" flag { name: "enable_rlp_callbacks_in_media_router2" + is_exported: true namespace: "media_solutions" description: "Make RouteListingPreference getter and callbacks public in MediaRouter2." bug: "281067101" @@ -16,6 +17,7 @@ flag { flag { name: "enable_audio_policies_device_and_bluetooth_controller" + is_exported: true namespace: "media_solutions" description: "Use Audio Policies implementation for device and Bluetooth route controllers." bug: "280576228" @@ -44,6 +46,7 @@ flag { flag { name: "enable_new_media_route_2_info_types" + is_exported: true namespace: "media_solutions" description: "Enables the following type constants in MediaRoute2Info: CAR, COMPUTER, GAME_CONSOLE, SMARTPHONE, SMARTWATCH, TABLET, TABLET_DOCKED. Note that this doesn't gate any behavior. It only guards some API int symbols." bug: "301713440" @@ -51,6 +54,7 @@ flag { flag { name: "enable_privileged_routing_for_media_routing_control" + is_exported: true namespace: "media_solutions" description: "Allow access to privileged routing capabilities to MEDIA_ROUTING_CONTROL holders." bug: "305919655" @@ -58,6 +62,7 @@ flag { flag { name: "enable_cross_user_routing_in_media_router2" + is_exported: true namespace: "media_solutions" description: "Allows clients of privileged MediaRouter2 that hold INTERACT_ACROSS_USERS_FULL to control routing across users." bug: "288580225" @@ -72,6 +77,7 @@ flag { flag { name: "enable_built_in_speaker_route_suitability_statuses" + is_exported: true namespace: "media_solutions" description: "Make MediaRoute2Info provide information about routes suitability for transfer." bug: "279555229" @@ -79,6 +85,7 @@ flag { flag { name: "enable_notifying_activity_manager_with_media_session_status_change" + is_exported: true namespace: "media_solutions" description: "Notify ActivityManager with the changes in playback state of the media session." bug: "295518668" @@ -86,6 +93,7 @@ flag { flag { name: "enable_get_transferable_routes" + is_exported: true namespace: "media_solutions" description: "Exposes RoutingController#getTransferableRoutes() (previously hidden) to the public API." bug: "323154573" @@ -100,6 +108,7 @@ flag { flag { name: "enable_screen_off_scanning" + is_exported: true namespace: "media_solutions" description: "Enable new MediaRouter2 API to enable watch companion apps to scan while the phone screen is off." bug: "281072508" diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig index f1107059111c..1731e5e4335c 100644 --- a/media/java/android/media/tv/flags/media_tv.aconfig +++ b/media/java/android/media/tv/flags/media_tv.aconfig @@ -2,6 +2,7 @@ package: "android.media.tv.flags" flag { name: "broadcast_visibility_types" + is_exported: true namespace: "media_tv" description: "Constants for standardizing broadcast visibility types." bug: "222402395" @@ -9,6 +10,7 @@ flag { flag { name: "enable_ad_service_fw" + is_exported: true namespace: "media_tv" description: "Enable the TV client-side AD framework." bug: "303506816" @@ -16,6 +18,7 @@ flag { flag { name: "tiaf_v_apis" + is_exported: true namespace: "media_tv" description: "TIAF V3.0 APIs for Android V" bug: "303323657" diff --git a/media/jni/Android.bp b/media/jni/Android.bp index 94fce797f5d6..d6d74e8a087a 100644 --- a/media/jni/Android.bp +++ b/media/jni/Android.bp @@ -122,6 +122,9 @@ cc_library_shared { "-Wunused", "-Wunreachable-code", ], + + // TODO(b/330503129) Workaround build breakage. + lto_O0: true, } cc_library_shared { diff --git a/media/jni/audioeffect/Android.bp b/media/jni/audioeffect/Android.bp index cf5059ceb3c9..7caa9e4863f9 100644 --- a/media/jni/audioeffect/Android.bp +++ b/media/jni/audioeffect/Android.bp @@ -44,4 +44,7 @@ cc_library_shared { "-Wunreachable-code", "-DANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION", ], + + // TODO(b/330503129) Workaround LTO build breakage. + lto_O0: true, } diff --git a/native/android/OWNERS b/native/android/OWNERS index 0b86909929b0..9a3527da9623 100644 --- a/native/android/OWNERS +++ b/native/android/OWNERS @@ -16,6 +16,8 @@ per-file system_fonts.cpp = file:/graphics/java/android/graphics/fonts/OWNERS per-file native_window_jni.cpp = file:/services/core/java/com/android/server/wm/OWNERS per-file native_activity.cpp = file:/services/core/java/com/android/server/wm/OWNERS per-file surface_control.cpp = file:/services/core/java/com/android/server/wm/OWNERS +per-file surface_control_input_receiver.cpp = file:/services/core/java/com/android/server/wm/OWNERS +per-file input_transfer_token.cpp = file:/services/core/java/com/android/server/wm/OWNERS # Graphics per-file choreographer.cpp = file:/graphics/java/android/graphics/OWNERS diff --git a/native/android/surface_control_input_receiver.cpp b/native/android/surface_control_input_receiver.cpp index da0defd9fd17..a84ec7309a62 100644 --- a/native/android/surface_control_input_receiver.cpp +++ b/native/android/surface_control_input_receiver.cpp @@ -45,6 +45,8 @@ public: mClientToken(clientToken), mInputTransferToken(inputTransferToken) {} + // The InputConsumer does not keep the InputReceiver alive so the receiver is cleared once the + // owner releases it. ~InputReceiver() { remove(); } @@ -190,7 +192,9 @@ const AInputTransferToken* AInputReceiver_getInputTransferToken(AInputReceiver* void AInputReceiver_release(AInputReceiver* aInputReceiver) { InputReceiver* inputReceiver = AInputReceiver_to_InputReceiver(aInputReceiver); - inputReceiver->remove(); + if (inputReceiver != nullptr) { + inputReceiver->remove(); + } delete inputReceiver; } diff --git a/nfc/Android.bp b/nfc/Android.bp index 7698e2b2d054..0b3f291a49de 100644 --- a/nfc/Android.bp +++ b/nfc/Android.bp @@ -50,7 +50,7 @@ java_sdk_library { ], defaults: ["framework-module-defaults"], sdk_version: "module_current", - min_sdk_version: "34", // should be 35 (making it 34 for compiling for `-next`) + min_sdk_version: "current", installable: true, optimize: { enabled: false, diff --git a/nfc/api/current.txt b/nfc/api/current.txt index da292a818396..80b2be2567a7 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -268,10 +268,9 @@ package android.nfc.cardemulation { } @FlaggedApi("android.nfc.nfc_read_polling_loop") public final class PollingFrame implements android.os.Parcelable { - ctor public PollingFrame(int, @Nullable byte[], int, int, boolean); method public int describeContents(); method @NonNull public byte[] getData(); - method public int getTimestamp(); + method public long getTimestamp(); method public boolean getTriggeredAutoTransact(); method public int getType(); method public int getVendorSpecificGain(); diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl index c444740a5b1b..7a78f3d17e1e 100644 --- a/nfc/java/android/nfc/INfcAdapter.aidl +++ b/nfc/java/android/nfc/INfcAdapter.aidl @@ -47,8 +47,8 @@ interface INfcAdapter INfcAdapterExtras getNfcAdapterExtrasInterface(in String pkg); INfcDta getNfcDtaInterface(in String pkg); int getState(); - boolean disable(boolean saveState); - boolean enable(); + boolean disable(boolean saveState, in String pkg); + boolean enable(in String pkg); void pausePolling(int timeoutInMs); void resumePolling(); diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 0ebc3f5178e0..7a7db31fd417 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -1117,7 +1117,7 @@ public final class NfcAdapter { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable() { try { - return sService.enable(); + return sService.enable(mContext.getPackageName()); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time @@ -1126,7 +1126,7 @@ public final class NfcAdapter { return false; } try { - return sService.enable(); + return sService.enable(mContext.getPackageName()); } catch (RemoteException ee) { Log.e(TAG, "Failed to recover NFC Service."); } @@ -1156,7 +1156,7 @@ public final class NfcAdapter { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable() { try { - return sService.disable(true); + return sService.disable(true, mContext.getPackageName()); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time @@ -1165,7 +1165,7 @@ public final class NfcAdapter { return false; } try { - return sService.disable(true); + return sService.disable(true, mContext.getPackageName()); } catch (RemoteException ee) { Log.e(TAG, "Failed to recover NFC Service."); } @@ -1181,7 +1181,7 @@ public final class NfcAdapter { @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean disable(boolean persist) { try { - return sService.disable(persist); + return sService.disable(persist, mContext.getPackageName()); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time @@ -1190,7 +1190,7 @@ public final class NfcAdapter { return false; } try { - return sService.disable(persist); + return sService.disable(persist, mContext.getPackageName()); } catch (RemoteException ee) { Log.e(TAG, "Failed to recover NFC Service."); } diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java index be3c24806c5b..572e20d1d9f8 100644 --- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -723,6 +723,8 @@ public final class ApduServiceInfo implements Parcelable { * delivered to {@link HostApduService#processPollingFrames(List)}. Adding a key with this * multiple times will cause the value to be overwritten each time. * @param pollingLoopFilter the polling loop filter to add, must be a valid hexadecimal string + * @param autoTransact when true, disable observe mode when this filter matches, when false, + * matching does not change the observe mode state */ @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void addPollingLoopFilter(@NonNull String pollingLoopFilter, @@ -747,6 +749,8 @@ public final class ApduServiceInfo implements Parcelable { * multiple times will cause the value to be overwritten each time. * @param pollingLoopPatternFilter the polling loop pattern filter to add, must be a valid * regex to match a hexadecimal string + * @param autoTransact when true, disable observe mode when this filter matches, when false, + * matching does not change the observe mode state */ @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void addPollingLoopPatternFilter(@NonNull String pollingLoopPatternFilter, diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java index af63a6e4350b..654e8cc574ba 100644 --- a/nfc/java/android/nfc/cardemulation/PollingFrame.java +++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java @@ -16,6 +16,7 @@ package android.nfc.cardemulation; +import android.annotation.DurationMillisLong; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -148,7 +149,8 @@ public final class PollingFrame implements Parcelable{ private final int mType; private final byte[] mData; private final int mGain; - private final int mTimestamp; + @DurationMillisLong + private final long mTimestamp; private final boolean mTriggeredAutoTransact; public static final @NonNull Parcelable.Creator<PollingFrame> CREATOR = @@ -180,16 +182,18 @@ public final class PollingFrame implements Parcelable{ * @param type the type of the frame * @param data a byte array of the data contained in the frame * @param gain the vendor-specific gain of the field - * @param timestamp the timestamp in millisecones + * @param timestampMillis the timestamp in millisecones * @param triggeredAutoTransact whether or not this frame triggered the device to start a * transaction automatically + * + * @hide */ public PollingFrame(@PollingFrameType int type, @Nullable byte[] data, - int gain, int timestamp, boolean triggeredAutoTransact) { + int gain, @DurationMillisLong long timestampMillis, boolean triggeredAutoTransact) { mType = type; mData = data == null ? new byte[0] : data; mGain = gain; - mTimestamp = timestamp; + mTimestamp = timestampMillis; mTriggeredAutoTransact = triggeredAutoTransact; } @@ -230,7 +234,7 @@ public final class PollingFrame implements Parcelable{ * frames relative to each other. * @return the timestamp in milliseconds */ - public int getTimestamp() { + public @DurationMillisLong long getTimestamp() { return mTimestamp; } @@ -264,7 +268,7 @@ public final class PollingFrame implements Parcelable{ frame.putInt(KEY_POLLING_LOOP_GAIN, (byte) getVendorSpecificGain()); } frame.putByteArray(KEY_POLLING_LOOP_DATA, getData()); - frame.putInt(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp()); + frame.putLong(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp()); frame.putBoolean(KEY_POLLING_LOOP_TRIGGERED_AUTOTRANSACT, getTriggeredAutoTransact()); return frame; } @@ -273,7 +277,7 @@ public final class PollingFrame implements Parcelable{ public String toString() { return "PollingFrame { Type: " + (char) getType() + ", gain: " + getVendorSpecificGain() - + ", timestamp: " + Integer.toUnsignedString(getTimestamp()) + + ", timestamp: " + Long.toUnsignedString(getTimestamp()) + ", data: [" + HexFormat.ofDelimiter(" ").formatHex(getData()) + "] }"; } } diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig index ba084c0901c4..6d4a17c27da0 100644 --- a/nfc/java/android/nfc/flags.aconfig +++ b/nfc/java/android/nfc/flags.aconfig @@ -63,6 +63,7 @@ flag { flag { name: "enable_nfc_charging" + is_exported: true namespace: "nfc" description: "Flag for NFC charging changes" bug: "292143899" diff --git a/packages/CrashRecovery/aconfig/flags.aconfig b/packages/CrashRecovery/aconfig/flags.aconfig index 572a66922ea3..8627eac7beed 100644 --- a/packages/CrashRecovery/aconfig/flags.aconfig +++ b/packages/CrashRecovery/aconfig/flags.aconfig @@ -10,6 +10,7 @@ flag { flag { name: "enable_crashrecovery" + is_exported: true namespace: "crashrecovery" description: "Enables various dependencies of crashrecovery module" bug: "289203818" diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java index 37b5d408a508..a8d8f9a1a55d 100644 --- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java +++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; import android.net.ConnectivityModuleConnector; import android.os.Environment; import android.os.Handler; @@ -57,16 +58,20 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -130,8 +135,25 @@ public class PackageWatchdog { @VisibleForTesting static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5; - @VisibleForTesting + static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10); + // Boot loop at which packageWatchdog starts first mitigation + private static final String BOOT_LOOP_THRESHOLD = + "persist.device_config.configuration.boot_loop_threshold"; + @VisibleForTesting + static final int DEFAULT_BOOT_LOOP_THRESHOLD = 15; + // Once boot_loop_threshold is surpassed next mitigation would be triggered after + // specified number of reboots. + private static final String BOOT_LOOP_MITIGATION_INCREMENT = + "persist.device_config.configuration..boot_loop_mitigation_increment"; + @VisibleForTesting + static final int DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT = 2; + + // Threshold level at which or above user might experience significant disruption. + private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD = + "persist.device_config.configuration.major_user_impact_level_threshold"; + private static final int DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD = + PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; private long mNumberOfNativeCrashPollsRemaining; @@ -145,6 +167,7 @@ public class PackageWatchdog { private static final String ATTR_EXPLICIT_HEALTH_CHECK_DURATION = "health-check-duration"; private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check"; private static final String ATTR_MITIGATION_CALLS = "mitigation-calls"; + private static final String ATTR_MITIGATION_COUNT = "mitigation-count"; // A file containing information about the current mitigation count in the case of a boot loop. // This allows boot loop information to persist in the case of an fs-checkpoint being @@ -230,8 +253,16 @@ public class PackageWatchdog { mConnectivityModuleConnector = connectivityModuleConnector; mSystemClock = clock; mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS; - mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT, - DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS); + if (Flags.recoverabilityDetection()) { + mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, + SystemProperties.getInt(BOOT_LOOP_MITIGATION_INCREMENT, + DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + } else { + mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS); + } + loadFromFile(); sPackageWatchdog = this; } @@ -436,8 +467,13 @@ public class PackageWatchdog { mitigationCount = currentMonitoredPackage.getMitigationCountLocked(); } - currentObserverToNotify.execute(versionedPackage, - failureReason, mitigationCount); + if (Flags.recoverabilityDetection()) { + maybeExecute(currentObserverToNotify, versionedPackage, + failureReason, currentObserverImpact, mitigationCount); + } else { + currentObserverToNotify.execute(versionedPackage, + failureReason, mitigationCount); + } } } } @@ -467,37 +503,76 @@ public class PackageWatchdog { } } if (currentObserverToNotify != null) { - currentObserverToNotify.execute(failingPackage, failureReason, 1); + if (Flags.recoverabilityDetection()) { + maybeExecute(currentObserverToNotify, failingPackage, failureReason, + currentObserverImpact, /*mitigationCount=*/ 1); + } else { + currentObserverToNotify.execute(failingPackage, failureReason, 1); + } + } + } + + private void maybeExecute(PackageHealthObserver currentObserverToNotify, + VersionedPackage versionedPackage, + @FailureReasons int failureReason, + int currentObserverImpact, + int mitigationCount) { + if (currentObserverImpact < getUserImpactLevelLimit()) { + currentObserverToNotify.execute(versionedPackage, failureReason, mitigationCount); } } + /** * Called when the system server boots. If the system server is detected to be in a boot loop, * query each observer and perform the mitigation action with the lowest user impact. */ + @SuppressWarnings("GuardedBy") public void noteBoot() { synchronized (mLock) { - if (mBootThreshold.incrementAndTest()) { - mBootThreshold.reset(); + boolean mitigate = mBootThreshold.incrementAndTest(); + if (mitigate) { + if (!Flags.recoverabilityDetection()) { + mBootThreshold.reset(); + } int mitigationCount = mBootThreshold.getMitigationCount() + 1; PackageHealthObserver currentObserverToNotify = null; + ObserverInternal currentObserverInternal = null; int currentObserverImpact = Integer.MAX_VALUE; for (int i = 0; i < mAllObservers.size(); i++) { final ObserverInternal observer = mAllObservers.valueAt(i); PackageHealthObserver registeredObserver = observer.registeredObserver; if (registeredObserver != null) { - int impact = registeredObserver.onBootLoop(mitigationCount); + int impact = Flags.recoverabilityDetection() + ? registeredObserver.onBootLoop( + observer.getBootMitigationCount() + 1) + : registeredObserver.onBootLoop(mitigationCount); if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 && impact < currentObserverImpact) { currentObserverToNotify = registeredObserver; + currentObserverInternal = observer; currentObserverImpact = impact; } } } if (currentObserverToNotify != null) { - mBootThreshold.setMitigationCount(mitigationCount); - mBootThreshold.saveMitigationCountToMetadata(); - currentObserverToNotify.executeBootLoopMitigation(mitigationCount); + if (Flags.recoverabilityDetection()) { + if (currentObserverImpact < getUserImpactLevelLimit() + || (currentObserverImpact >= getUserImpactLevelLimit() + && mBootThreshold.getCount() >= getBootLoopThreshold())) { + int currentObserverMitigationCount = + currentObserverInternal.getBootMitigationCount() + 1; + currentObserverInternal.setBootMitigationCount( + currentObserverMitigationCount); + saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); + currentObserverToNotify.executeBootLoopMitigation( + currentObserverMitigationCount); + } + } else { + mBootThreshold.setMitigationCount(mitigationCount); + mBootThreshold.saveMitigationCountToMetadata(); + currentObserverToNotify.executeBootLoopMitigation(mitigationCount); + } } } } @@ -567,13 +642,27 @@ public class PackageWatchdog { mShortTaskHandler.post(()->checkAndMitigateNativeCrashes()); } + private int getUserImpactLevelLimit() { + return SystemProperties.getInt(MAJOR_USER_IMPACT_LEVEL_THRESHOLD, + DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD); + } + + private int getBootLoopThreshold() { + return SystemProperties.getInt(BOOT_LOOP_THRESHOLD, + DEFAULT_BOOT_LOOP_THRESHOLD); + } + /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */ @Retention(SOURCE) @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0, PackageHealthObserverImpact.USER_IMPACT_LEVEL_10, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20, PackageHealthObserverImpact.USER_IMPACT_LEVEL_30, PackageHealthObserverImpact.USER_IMPACT_LEVEL_50, PackageHealthObserverImpact.USER_IMPACT_LEVEL_70, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_71, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_75, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_80, PackageHealthObserverImpact.USER_IMPACT_LEVEL_90, PackageHealthObserverImpact.USER_IMPACT_LEVEL_100}) public @interface PackageHealthObserverImpact { @@ -582,11 +671,15 @@ public class PackageWatchdog { /* Action has low user impact, user of a device will barely notice. */ int USER_IMPACT_LEVEL_10 = 10; /* Actions having medium user impact, user of a device will likely notice. */ + int USER_IMPACT_LEVEL_20 = 20; int USER_IMPACT_LEVEL_30 = 30; int USER_IMPACT_LEVEL_50 = 50; int USER_IMPACT_LEVEL_70 = 70; - int USER_IMPACT_LEVEL_90 = 90; /* Action has high user impact, a last resort, user of a device will be very frustrated. */ + int USER_IMPACT_LEVEL_71 = 71; + int USER_IMPACT_LEVEL_75 = 75; + int USER_IMPACT_LEVEL_80 = 80; + int USER_IMPACT_LEVEL_90 = 90; int USER_IMPACT_LEVEL_100 = 100; } @@ -1144,6 +1237,12 @@ public class PackageWatchdog { } } + @VisibleForTesting + @GuardedBy("mLock") + void registerObserverInternal(ObserverInternal observerInternal) { + mAllObservers.put(observerInternal.name, observerInternal); + } + /** * Represents an observer monitoring a set of packages along with the failure thresholds for * each package. @@ -1151,17 +1250,23 @@ public class PackageWatchdog { * <p> Note, the PackageWatchdog#mLock must always be held when reading or writing * instances of this class. */ - private static class ObserverInternal { + static class ObserverInternal { public final String name; @GuardedBy("mLock") private final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>(); @Nullable @GuardedBy("mLock") public PackageHealthObserver registeredObserver; + private int mMitigationCount; ObserverInternal(String name, List<MonitoredPackage> packages) { + this(name, packages, /*mitigationCount=*/ 0); + } + + ObserverInternal(String name, List<MonitoredPackage> packages, int mitigationCount) { this.name = name; updatePackagesLocked(packages); + this.mMitigationCount = mitigationCount; } /** @@ -1173,6 +1278,9 @@ public class PackageWatchdog { try { out.startTag(null, TAG_OBSERVER); out.attribute(null, ATTR_NAME, name); + if (Flags.recoverabilityDetection()) { + out.attributeInt(null, ATTR_MITIGATION_COUNT, mMitigationCount); + } for (int i = 0; i < mPackages.size(); i++) { MonitoredPackage p = mPackages.valueAt(i); p.writeLocked(out); @@ -1185,6 +1293,14 @@ public class PackageWatchdog { } } + public int getBootMitigationCount() { + return mMitigationCount; + } + + public void setBootMitigationCount(int mitigationCount) { + mMitigationCount = mitigationCount; + } + @GuardedBy("mLock") public void updatePackagesLocked(List<MonitoredPackage> packages) { for (int pIndex = 0; pIndex < packages.size(); pIndex++) { @@ -1289,6 +1405,7 @@ public class PackageWatchdog { **/ public static ObserverInternal read(TypedXmlPullParser parser, PackageWatchdog watchdog) { String observerName = null; + int observerMitigationCount = 0; if (TAG_OBSERVER.equals(parser.getName())) { observerName = parser.getAttributeValue(null, ATTR_NAME); if (TextUtils.isEmpty(observerName)) { @@ -1299,6 +1416,9 @@ public class PackageWatchdog { List<MonitoredPackage> packages = new ArrayList<>(); int innerDepth = parser.getDepth(); try { + if (Flags.recoverabilityDetection()) { + observerMitigationCount = parser.getAttributeInt(null, ATTR_MITIGATION_COUNT); + } while (XmlUtils.nextElementWithin(parser, innerDepth)) { if (TAG_PACKAGE.equals(parser.getName())) { try { @@ -1319,7 +1439,7 @@ public class PackageWatchdog { if (packages.isEmpty()) { return null; } - return new ObserverInternal(observerName, packages); + return new ObserverInternal(observerName, packages, observerMitigationCount); } /** Dumps information about this observer and the packages it watches. */ @@ -1679,6 +1799,27 @@ public class PackageWatchdog { } } + @GuardedBy("mLock") + @SuppressWarnings("GuardedBy") + void saveAllObserversBootMitigationCountToMetadata(String filePath) { + HashMap<String, Integer> bootMitigationCounts = new HashMap<>(); + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + bootMitigationCounts.put(observer.name, observer.getBootMitigationCount()); + } + + try { + FileOutputStream fileStream = new FileOutputStream(new File(filePath)); + ObjectOutputStream objectStream = new ObjectOutputStream(fileStream); + objectStream.writeObject(bootMitigationCounts); + objectStream.flush(); + objectStream.close(); + fileStream.close(); + } catch (Exception e) { + Slog.i(TAG, "Could not save observers metadata to file: " + e); + } + } + /** * Handles the thresholding logic for system server boots. */ @@ -1686,10 +1827,16 @@ public class PackageWatchdog { private final int mBootTriggerCount; private final long mTriggerWindow; + private final int mBootMitigationIncrement; BootThreshold(int bootTriggerCount, long triggerWindow) { + this(bootTriggerCount, triggerWindow, /*bootMitigationIncrement=*/ 1); + } + + BootThreshold(int bootTriggerCount, long triggerWindow, int bootMitigationIncrement) { this.mBootTriggerCount = bootTriggerCount; this.mTriggerWindow = triggerWindow; + this.mBootMitigationIncrement = bootMitigationIncrement; } public void reset() { @@ -1761,8 +1908,13 @@ public class PackageWatchdog { /** Increments the boot counter, and returns whether the device is bootlooping. */ + @GuardedBy("mLock") public boolean incrementAndTest() { - readMitigationCountFromMetadataIfNecessary(); + if (Flags.recoverabilityDetection()) { + readAllObserversBootMitigationCountIfNecessary(METADATA_FILE); + } else { + readMitigationCountFromMetadataIfNecessary(); + } final long now = mSystemClock.uptimeMillis(); if (now - getStart() < 0) { Slog.e(TAG, "Window was less than zero. Resetting start to current time."); @@ -1770,8 +1922,12 @@ public class PackageWatchdog { setMitigationStart(now); } if (now - getMitigationStart() > DEFAULT_DEESCALATION_WINDOW_MS) { - setMitigationCount(0); setMitigationStart(now); + if (Flags.recoverabilityDetection()) { + resetAllObserversBootMitigationCount(); + } else { + setMitigationCount(0); + } } final long window = now - getStart(); if (window >= mTriggerWindow) { @@ -1782,9 +1938,48 @@ public class PackageWatchdog { int count = getCount() + 1; setCount(count); EventLogTags.writeRescueNote(Process.ROOT_UID, count, window); + if (Flags.recoverabilityDetection()) { + boolean mitigate = (count >= mBootTriggerCount) + && (count - mBootTriggerCount) % mBootMitigationIncrement == 0; + return mitigate; + } return count >= mBootTriggerCount; } } + @GuardedBy("mLock") + private void resetAllObserversBootMitigationCount() { + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + observer.setBootMitigationCount(0); + } + } + + @GuardedBy("mLock") + @SuppressWarnings("GuardedBy") + void readAllObserversBootMitigationCountIfNecessary(String filePath) { + File metadataFile = new File(filePath); + if (metadataFile.exists()) { + try { + FileInputStream fileStream = new FileInputStream(metadataFile); + ObjectInputStream objectStream = new ObjectInputStream(fileStream); + HashMap<String, Integer> bootMitigationCounts = + (HashMap<String, Integer>) objectStream.readObject(); + objectStream.close(); + fileStream.close(); + + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + if (bootMitigationCounts.containsKey(observer.name)) { + observer.setBootMitigationCount( + bootMitigationCounts.get(observer.name)); + } + } + } catch (Exception e) { + Slog.i(TAG, "Could not read observer metadata file: " + e); + } + } + } + } } diff --git a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java index 7bdc1a0e3ac7..7093ba42f40d 100644 --- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java +++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java @@ -20,6 +20,7 @@ import static android.provider.DeviceConfig.Properties; import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ContentResolver; @@ -27,6 +28,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; import android.os.Build; import android.os.Environment; import android.os.PowerManager; @@ -53,6 +55,8 @@ import com.android.server.am.SettingsToPropertiesMapper; import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -89,6 +93,40 @@ public class RescueParty { @VisibleForTesting static final int LEVEL_FACTORY_RESET = 5; @VisibleForTesting + static final int RESCUE_LEVEL_NONE = 0; + @VisibleForTesting + static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1; + @VisibleForTesting + static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2; + @VisibleForTesting + static final int RESCUE_LEVEL_WARM_REBOOT = 3; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6; + @VisibleForTesting + static final int RESCUE_LEVEL_FACTORY_RESET = 7; + + @IntDef(prefix = { "RESCUE_LEVEL_" }, value = { + RESCUE_LEVEL_NONE, + RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET, + RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET, + RESCUE_LEVEL_WARM_REBOOT, + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS, + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES, + RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS, + RESCUE_LEVEL_FACTORY_RESET + }) + @Retention(RetentionPolicy.SOURCE) + @interface RescueLevels {} + + @VisibleForTesting + static final String RESCUE_NON_REBOOT_LEVEL_LIMIT = "persist.sys.rescue_non_reboot_level_limit"; + @VisibleForTesting + static final int DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT = RESCUE_LEVEL_WARM_REBOOT - 1; + @VisibleForTesting static final String TAG = "RescueParty"; @VisibleForTesting static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2); @@ -347,11 +385,20 @@ public class RescueParty { } private static int getMaxRescueLevel(boolean mayPerformReboot) { - if (!mayPerformReboot - || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { - return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS; + if (Flags.recoverabilityDetection()) { + if (!mayPerformReboot + || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { + return SystemProperties.getInt(RESCUE_NON_REBOOT_LEVEL_LIMIT, + DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT); + } + return RESCUE_LEVEL_FACTORY_RESET; + } else { + if (!mayPerformReboot + || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { + return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS; + } + return LEVEL_FACTORY_RESET; } - return LEVEL_FACTORY_RESET; } /** @@ -379,6 +426,46 @@ public class RescueParty { } } + /** + * Get the rescue level to perform if this is the n-th attempt at mitigating failure. + * When failedPackage is null then 1st and 2nd mitigation counts are redundant (scoped and + * all device config reset). Behaves as if one mitigation attempt was already done. + * + * @param mitigationCount the mitigation attempt number (1 = first attempt etc.). + * @param mayPerformReboot whether or not a reboot and factory reset may be performed + * for the given failure. + * @param failedPackage in case of bootloop this is null. + * @return the rescue level for the n-th mitigation attempt. + */ + private static @RescueLevels int getRescueLevel(int mitigationCount, boolean mayPerformReboot, + @Nullable VersionedPackage failedPackage) { + // Skipping RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET since it's not defined without a failed + // package. + if (failedPackage == null && mitigationCount > 0) { + mitigationCount += 1; + } + if (mitigationCount == 1) { + return RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET; + } else if (mitigationCount == 2) { + return RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET; + } else if (mitigationCount == 3) { + return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_WARM_REBOOT); + } else if (mitigationCount == 4) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS); + } else if (mitigationCount == 5) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES); + } else if (mitigationCount == 6) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS); + } else if (mitigationCount >= 7) { + return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_FACTORY_RESET); + } else { + return RESCUE_LEVEL_NONE; + } + } + private static void executeRescueLevel(Context context, @Nullable String failedPackage, int level) { Slog.w(TAG, "Attempting rescue level " + levelToString(level)); @@ -397,6 +484,15 @@ public class RescueParty { private static void executeRescueLevelInternal(Context context, int level, @Nullable String failedPackage) throws Exception { + if (Flags.recoverabilityDetection()) { + executeRescueLevelInternalNew(context, level, failedPackage); + } else { + executeRescueLevelInternalOld(context, level, failedPackage); + } + } + + private static void executeRescueLevelInternalOld(Context context, int level, @Nullable + String failedPackage) throws Exception { if (level <= LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS) { // Disabling flag resets on master branch for trunk stable launch. @@ -410,8 +506,6 @@ public class RescueParty { // Try our best to reset all settings possible, and once finished // rethrow any exception that we encountered Exception res = null; - Runnable runnable; - Thread thread; switch (level) { case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: try { @@ -453,21 +547,7 @@ public class RescueParty { } break; case LEVEL_WARM_REBOOT: - // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog - // when device shutting down. - setRebootProperty(true); - runnable = () -> { - try { - PowerManager pm = context.getSystemService(PowerManager.class); - if (pm != null) { - pm.reboot(TAG); - } - } catch (Throwable t) { - logRescueException(level, failedPackage, t); - } - }; - thread = new Thread(runnable); - thread.start(); + executeWarmReboot(context, level, failedPackage); break; case LEVEL_FACTORY_RESET: // Before the completion of Reboot, if any crash happens then PackageWatchdog @@ -475,23 +555,9 @@ public class RescueParty { // Adding a check to prevent factory reset to execute before above reboot completes. // Note: this reboot property is not persistent resets after reboot is completed. if (isRebootPropertySet()) { - break; + return; } - setFactoryResetProperty(true); - long now = System.currentTimeMillis(); - setLastFactoryResetTimeMs(now); - runnable = new Runnable() { - @Override - public void run() { - try { - RecoverySystem.rebootPromptAndWipeUserData(context, TAG); - } catch (Throwable t) { - logRescueException(level, failedPackage, t); - } - } - }; - thread = new Thread(runnable); - thread.start(); + executeFactoryReset(context, level, failedPackage); break; } @@ -500,6 +566,83 @@ public class RescueParty { } } + private static void executeRescueLevelInternalNew(Context context, @RescueLevels int level, + @Nullable String failedPackage) throws Exception { + CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED, + level, levelToString(level)); + switch (level) { + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + // Temporary disable deviceConfig reset + // resetDeviceConfig(context, /*isScoped=*/true, failedPackage); + break; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + // Temporary disable deviceConfig reset + // resetDeviceConfig(context, /*isScoped=*/false, failedPackage); + break; + case RESCUE_LEVEL_WARM_REBOOT: + executeWarmReboot(context, level, failedPackage); + break; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_DEFAULTS, level); + break; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_CHANGES, level); + break; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + resetAllSettingsIfNecessary(context, Settings.RESET_MODE_TRUSTED_DEFAULTS, level); + break; + case RESCUE_LEVEL_FACTORY_RESET: + // Before the completion of Reboot, if any crash happens then PackageWatchdog + // escalates to next level i.e. factory reset, as they happen in separate threads. + // Adding a check to prevent factory reset to execute before above reboot completes. + // Note: this reboot property is not persistent resets after reboot is completed. + if (isRebootPropertySet()) { + return; + } + executeFactoryReset(context, level, failedPackage); + break; + } + } + + private static void executeWarmReboot(Context context, int level, + @Nullable String failedPackage) { + // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog + // when device shutting down. + setRebootProperty(true); + Runnable runnable = () -> { + try { + PowerManager pm = context.getSystemService(PowerManager.class); + if (pm != null) { + pm.reboot(TAG); + } + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + + private static void executeFactoryReset(Context context, int level, + @Nullable String failedPackage) { + setFactoryResetProperty(true); + long now = System.currentTimeMillis(); + setLastFactoryResetTimeMs(now); + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + RecoverySystem.rebootPromptAndWipeUserData(context, TAG); + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + + private static String getCompleteMessage(Throwable t) { final StringBuilder builder = new StringBuilder(); builder.append(t.getMessage()); @@ -521,17 +664,38 @@ public class RescueParty { } private static int mapRescueLevelToUserImpact(int rescueLevel) { - switch(rescueLevel) { - case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: - case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; - case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: - case LEVEL_WARM_REBOOT: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; - case LEVEL_FACTORY_RESET: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; - default: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + if (Flags.recoverabilityDetection()) { + switch (rescueLevel) { + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_20; + case RESCUE_LEVEL_WARM_REBOOT: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80; + case RESCUE_LEVEL_FACTORY_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; + default: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + } else { + switch (rescueLevel) { + case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; + case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + case LEVEL_WARM_REBOOT: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; + case LEVEL_FACTORY_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; + default: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } } } @@ -548,7 +712,7 @@ public class RescueParty { final ContentResolver resolver = context.getContentResolver(); try { Settings.Global.resetToDefaultsAsUser(resolver, null, mode, - UserHandle.SYSTEM.getIdentifier()); + UserHandle.SYSTEM.getIdentifier()); } catch (Exception e) { res = new RuntimeException("Failed to reset global settings", e); } @@ -667,8 +831,13 @@ public class RescueParty { @FailureReasons int failureReason, int mitigationCount) { if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) { - return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + if (Flags.recoverabilityDetection()) { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + mayPerformReboot(failedPackage), failedPackage)); + } else { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, mayPerformReboot(failedPackage))); + } } else { return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; } @@ -682,8 +851,10 @@ public class RescueParty { } if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) { - final int level = getRescueLevel(mitigationCount, - mayPerformReboot(failedPackage)); + final int level = Flags.recoverabilityDetection() ? getRescueLevel(mitigationCount, + mayPerformReboot(failedPackage), failedPackage) + : getRescueLevel(mitigationCount, + mayPerformReboot(failedPackage)); executeRescueLevel(mContext, failedPackage == null ? null : failedPackage.getPackageName(), level); return true; @@ -716,7 +887,12 @@ public class RescueParty { if (isDisabled()) { return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; } - return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true)); + if (Flags.recoverabilityDetection()) { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + true, /*failedPackage=*/ null)); + } else { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true)); + } } @Override @@ -725,8 +901,10 @@ public class RescueParty { return false; } boolean mayPerformReboot = !shouldThrottleReboot(); - executeRescueLevel(mContext, /*failedPackage=*/ null, - getRescueLevel(mitigationCount, mayPerformReboot)); + final int level = Flags.recoverabilityDetection() ? getRescueLevel(mitigationCount, + mayPerformReboot, /*failedPackage=*/ null) + : getRescueLevel(mitigationCount, mayPerformReboot); + executeRescueLevel(mContext, /*failedPackage=*/ null, level); return true; } @@ -843,14 +1021,44 @@ public class RescueParty { } private static String levelToString(int level) { - switch (level) { - case LEVEL_NONE: return "NONE"; - case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; - case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: return "RESET_SETTINGS_UNTRUSTED_CHANGES"; - case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: return "RESET_SETTINGS_TRUSTED_DEFAULTS"; - case LEVEL_WARM_REBOOT: return "WARM_REBOOT"; - case LEVEL_FACTORY_RESET: return "FACTORY_RESET"; - default: return Integer.toString(level); + if (Flags.recoverabilityDetection()) { + switch (level) { + case RESCUE_LEVEL_NONE: + return "NONE"; + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + return "SCOPED_DEVICE_CONFIG_RESET"; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + return "ALL_DEVICE_CONFIG_RESET"; + case RESCUE_LEVEL_WARM_REBOOT: + return "WARM_REBOOT"; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return "RESET_SETTINGS_UNTRUSTED_CHANGES"; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return "RESET_SETTINGS_TRUSTED_DEFAULTS"; + case RESCUE_LEVEL_FACTORY_RESET: + return "FACTORY_RESET"; + default: + return Integer.toString(level); + } + } else { + switch (level) { + case LEVEL_NONE: + return "NONE"; + case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; + case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return "RESET_SETTINGS_UNTRUSTED_CHANGES"; + case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return "RESET_SETTINGS_TRUSTED_DEFAULTS"; + case LEVEL_WARM_REBOOT: + return "WARM_REBOOT"; + case LEVEL_FACTORY_RESET: + return "FACTORY_RESET"; + default: + return Integer.toString(level); + } } } } diff --git a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java index 0fb932735ab4..93f26aefb692 100644 --- a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java +++ b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java @@ -69,7 +69,7 @@ import java.util.function.Consumer; * * @hide */ -final class RollbackPackageHealthObserver implements PackageHealthObserver { +public final class RollbackPackageHealthObserver implements PackageHealthObserver { private static final String TAG = "RollbackPackageHealthObserver"; private static final String NAME = "rollback-observer"; private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT @@ -89,7 +89,7 @@ final class RollbackPackageHealthObserver implements PackageHealthObserver { private boolean mTwoPhaseRollbackEnabled; @VisibleForTesting - RollbackPackageHealthObserver(Context context, ApexManager apexManager) { + public RollbackPackageHealthObserver(Context context, ApexManager apexManager) { mContext = context; HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver"); handlerThread.start(); diff --git a/packages/CredentialManager/AndroidManifest.xml b/packages/CredentialManager/AndroidManifest.xml index a5ccdb6575bb..7a8c25bd12ab 100644 --- a/packages/CredentialManager/AndroidManifest.xml +++ b/packages/CredentialManager/AndroidManifest.xml @@ -23,6 +23,7 @@ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/> <uses-permission android:name="android.permission.HIDE_NON_SYSTEM_OVERLAY_WINDOWS"/> <uses-permission android:name="android.permission.ACCESS_INSTANT_APPS" /> + <uses-permission android:name="android.permission.USE_BIOMETRIC" /> <application android:allowBackup="true" diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml index 7f09dd5d07cc..914987ac4650 100644 --- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml +++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml @@ -33,10 +33,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:paddingLeft="@dimen/autofill_view_left_padding" + android:paddingStart="@dimen/autofill_view_left_padding" android:src="@drawable/more_horiz_24px" android:tint="?androidprv:attr/materialColorOnSurface" - android:layout_alignParentStart="true" android:contentDescription="@string/more_options_content_description" android:background="@null"/> @@ -44,8 +43,8 @@ android:id="@+id/text_container" android:layout_width="@dimen/autofill_dropdown_textview_max_width" android:layout_height="wrap_content" - android:paddingLeft="@dimen/autofill_view_left_padding" - android:paddingRight="@dimen/autofill_view_right_padding" + android:paddingStart="@dimen/autofill_view_left_padding" + android:paddingEnd="@dimen/autofill_view_right_padding" android:paddingTop="@dimen/more_options_item_vertical_padding" android:paddingBottom="@dimen/more_options_item_vertical_padding" android:orientation="vertical"> @@ -54,9 +53,7 @@ android:id="@android:id/text1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentTop="true" android:textColor="?androidprv:attr/materialColorOnSurface" - android:layout_toEndOf="@android:id/icon1" style="@style/autofill.TextTitle"/> </LinearLayout> diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml index 08948d793488..e998fe8fc8d9 100644 --- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml +++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml @@ -42,8 +42,8 @@ android:id="@+id/text_container" android:layout_width="@dimen/autofill_dropdown_textview_max_width" android:layout_height="wrap_content" - android:paddingLeft="@dimen/autofill_view_left_padding" - android:paddingRight="@dimen/autofill_view_right_padding" + android:paddingStart="@dimen/autofill_view_left_padding" + android:paddingEnd="@dimen/autofill_view_right_padding" android:paddingTop="@dimen/autofill_view_top_padding" android:paddingBottom="@dimen/autofill_view_bottom_padding" android:orientation="vertical"> @@ -52,8 +52,6 @@ android:id="@android:id/text1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentTop="true" - android:layout_toEndOf="@android:id/icon1" android:textColor="?androidprv:attr/materialColorOnSurface" style="@style/autofill.TextTitle"/> @@ -61,8 +59,6 @@ android:id="@android:id/text2" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_below="@android:id/text1" - android:layout_toEndOf="@android:id/icon1" android:textColor="?androidprv:attr/materialColorOnSurfaceVariant" style="@style/autofill.TextSubtitle"/> diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 527701c06bc6..bc35a85e48f8 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -63,11 +63,11 @@ <!-- This appears as the description body of the modal bottom sheet which provides all available providers for users to choose. [CHAR LIMIT=200] --> <string name="choose_provider_body">Select a password manager to save your info and sign in faster next time</string> <!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is passkey. [CHAR LIMIT=200] --> - <string name="choose_create_option_passkey_title">Create passkey to sign in to <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string> + <string name="choose_create_option_passkey_title">Create passkey to sign in to <xliff:g id="app_name" example="Tribank">%1$s</xliff:g>?</string> <!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is password. [CHAR LIMIT=200] --> - <string name="choose_create_option_password_title">Save password to sign in to <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string> + <string name="choose_create_option_password_title">Save password to sign in to <xliff:g id="app_name" example="Tribank">%1$s</xliff:g>?</string> <!-- This appears as the title of the modal bottom sheet for users to choose the create option inside a provider when the credential type is others. [CHAR LIMIT=200] --> - <string name="choose_create_option_sign_in_title">Save sign-in info for <xliff:g id="appName" example="Tribank">%1$s</xliff:g>?</string> + <string name="choose_create_option_sign_in_title">Save sign-in info for <xliff:g id="app_name" example="Tribank">%1$s</xliff:g>?</string> <!-- Types which are inserted as a placeholder as credentialTypes for other strings. [CHAR LIMIT=200] --> <string name="passkey">passkey</string> <string name="password">password</string> @@ -122,6 +122,8 @@ <string name="get_dialog_title_use_passkey_for">Use your saved passkey for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string> <!-- This appears as the title of the modal bottom sheet asking for user confirmation to use the single previously saved password to sign in to the app. [CHAR LIMIT=200] --> <string name="get_dialog_title_use_password_for">Use your saved password for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string> + <!-- This appears as a description of the modal bottom sheet when the single tap sign in flow is used for the get flow. [CHAR LIMIT=200] --> + <string name="get_dialog_title_single_tap_for">Use your screen lock to sign in to <xliff:g id="app_name" example="Shrine">%1$s</xliff:g> with <xliff:g id="username" example="beckett-bakery@gmail.com">%2$s</xliff:g></string> <!-- This appears as the title of the dialog asking for user confirmation to use the single user credential (previously saved or to be created) to sign in to the app. [CHAR LIMIT=200] --> <string name="get_dialog_title_use_sign_in_for">Use your sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string> <!-- This appears as the title of the dialog asking for user confirmation to unlock / authenticate (e.g. via fingerprint, faceId, passcode etc.) so that we can retrieve their sign-in options. [CHAR LIMIT=200] --> diff --git a/packages/CredentialManager/shared/AndroidManifest.xml b/packages/CredentialManager/shared/AndroidManifest.xml index a46088783024..51c7fb647355 100644 --- a/packages/CredentialManager/shared/AndroidManifest.xml +++ b/packages/CredentialManager/shared/AndroidManifest.xml @@ -17,6 +17,6 @@ */ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.credentialmanager"> + package="com.android.credentialmanager.shared"> </manifest> diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt index 892eabf14191..f2c252ec6422 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt @@ -40,14 +40,15 @@ import androidx.credentials.provider.PasswordCredentialEntry import androidx.credentials.provider.PublicKeyCredentialEntry import androidx.credentials.provider.RemoteEntry import com.android.credentialmanager.IS_AUTO_SELECTED_KEY -import com.android.credentialmanager.R import com.android.credentialmanager.model.get.ActionEntryInfo import com.android.credentialmanager.model.get.AuthenticationEntryInfo import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.model.get.ProviderInfo import com.android.credentialmanager.model.get.RemoteEntryInfo +import com.android.credentialmanager.shared.R import com.android.credentialmanager.TAG +import com.android.credentialmanager.model.BiometricRequestInfo import com.android.credentialmanager.model.EntryInfo fun EntryInfo.getIntentSenderRequest( @@ -139,6 +140,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), + biometricRequest = predetermineAndValidateBiometricFlow(it), ) ) } @@ -167,6 +169,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), + biometricRequest = predetermineAndValidateBiometricFlow(it), ) ) } @@ -194,6 +197,7 @@ private fun getCredentialOptionInfoList( isDefaultIconPreferredAsSingleProvider = credentialEntry.isDefaultIconPreferredAsSingleProvider, affiliatedDomain = credentialEntry.affiliatedDomain?.toString(), + biometricRequest = predetermineAndValidateBiometricFlow(it), ) ) } @@ -205,6 +209,36 @@ private fun getCredentialOptionInfoList( } return result } + +/** + * This validates if this is a biometric flow or not, and if it is, this returns the expected + * [BiometricRequestInfo]. Namely, the biometric flow must have at least the + * ALLOWED_AUTHENTICATORS bit passed from Jetpack. + * Note that the required values, such as the provider info's icon or display name, or the entries + * credential type or userName, and finally the display info's app name, are non-null and must + * exist to run through the flow. + * // TODO(b/326243754) : Presently, due to dependencies, the opId bit is parsed but is never + * // expected to be used. When it is added, it should be lightly validated. + */ +private fun predetermineAndValidateBiometricFlow( + it: Entry +): BiometricRequestInfo? { + // TODO(b/326243754) : When available, use the official jetpack structured type + val allowedAuthenticators: Int? = it.slice.items.firstOrNull { + it.hasHint("androidx.credentials." + + "provider.credentialEntry.SLICE_HINT_ALLOWED_AUTHENTICATORS") + }?.int + + // This is optional and does not affect validating the biometric flow in any case + val opId: Int? = it.slice.items.firstOrNull { + it.hasHint("androidx.credentials.provider.credentialEntry.SLICE_HINT_CRYPTO_OP_ID") + }?.int + if (allowedAuthenticators != null) { + return BiometricRequestInfo(opId = opId, allowedAuthenticators = allowedAuthenticators) + } + return null +} + val Slice.credentialEntry: CredentialEntry? get() = try { @@ -221,7 +255,6 @@ val Slice.credentialEntry: CredentialEntry? CustomCredentialEntry.fromSlice(this) } - /** * Note: caller required handle empty list due to parsing error. */ @@ -386,4 +419,4 @@ private fun getPackageInfo( PackageManager.PackageInfoFlags.of( (packageManagerFlags).toLong()) ) -}
\ No newline at end of file +} diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt new file mode 100644 index 000000000000..486cfe7123dd --- /dev/null +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/BiometricRequestInfo.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.credentialmanager.model + +/** + * This allows reading the data from the request, and holding that state around the framework. + * The [opId] bit is required for some authentication flows where CryptoObjects are used. + * The [allowedAuthenticators] is needed for all flows, and our flow ensures this value is never + * null. + */ +data class BiometricRequestInfo( + val opId: Int? = null, + val allowedAuthenticators: Int +)
\ No newline at end of file diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt index d6189eb15ff3..fe02e5ba026d 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/creation/CreateOptionInfo.kt @@ -19,6 +19,7 @@ package com.android.credentialmanager.model.creation import android.app.PendingIntent import android.content.Intent import android.graphics.drawable.Drawable +import com.android.credentialmanager.model.BiometricRequestInfo import com.android.credentialmanager.model.EntryInfo import java.time.Instant @@ -36,6 +37,7 @@ class CreateOptionInfo( val lastUsedTime: Instant, val footerDescription: String?, val allowAutoSelect: Boolean, + val biometricRequest: BiometricRequestInfo? = null, ) : EntryInfo( providerId, entryKey, diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt index a657e97de3cc..8913397db072 100644 --- a/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt +++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/model/get/CredentialEntryInfo.kt @@ -19,6 +19,7 @@ package com.android.credentialmanager.model.get import android.app.PendingIntent import android.content.Intent import android.graphics.drawable.Drawable +import com.android.credentialmanager.model.BiometricRequestInfo import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.model.EntryInfo import java.time.Instant @@ -49,6 +50,7 @@ class CredentialEntryInfo( // "For <value-of-entryGroupId>" on the more-option screen. val isDefaultIconPreferredAsSingleProvider: Boolean, val affiliatedDomain: String?, + val biometricRequest: BiometricRequestInfo? = null, ) : EntryInfo( providerId, entryKey, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 879d64c761ec..b17a98b30eee 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -40,6 +40,7 @@ import com.android.credentialmanager.getflow.GetCredentialUiState import com.android.credentialmanager.getflow.findAutoSelectEntry import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.createflow.isFlowAutoSelectable +import com.android.credentialmanager.getflow.findBiometricFlowEntry /** * Client for interacting with Credential Manager. Also holds data inputs from it. @@ -148,10 +149,17 @@ class CredentialManagerRepo( ) } RequestInfo.TYPE_GET -> { - val getCredentialInitialUiState = getCredentialInitialUiState(originName, + var getCredentialInitialUiState = getCredentialInitialUiState(originName, isReqForAllOptions)!! val autoSelectEntry = findAutoSelectEntry(getCredentialInitialUiState.providerDisplayInfo) + val biometricEntry = findBiometricFlowEntry( + getCredentialInitialUiState.providerDisplayInfo, + autoSelectEntry != null) + if (biometricEntry != null) { + getCredentialInitialUiState = getCredentialInitialUiState.copy( + activeEntry = biometricEntry) + } UiState( createCredentialUiState = null, getCredentialUiState = getCredentialInitialUiState, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 1f2fa200e43d..28c40479962e 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -17,6 +17,7 @@ package com.android.credentialmanager import android.app.Activity +import android.hardware.biometrics.BiometricPrompt import android.os.IBinder import android.text.TextUtils import android.util.Log @@ -28,6 +29,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import com.android.credentialmanager.common.BiometricResult +import com.android.credentialmanager.common.BiometricState import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.common.Constants import com.android.credentialmanager.common.DialogState @@ -54,6 +57,7 @@ data class UiState( val isAutoSelectFlow: Boolean = false, val cancelRequestState: CancelUiRequestState?, val isInitialRender: Boolean, + val biometricState: BiometricState = BiometricState() ) data class CancelUiRequestState( @@ -113,12 +117,21 @@ class CredentialSelectorViewModel( launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult> ) { val entry = uiState.selectedEntry + val biometricState = uiState.biometricState val pendingIntent = entry?.pendingIntent if (pendingIntent != null) { Log.d(Constants.LOG_TAG, "Launching provider activity") uiState = uiState.copy(providerActivityState = ProviderActivityState.PENDING) val entryIntent = entry.fillInIntent entryIntent?.putExtra(Constants.IS_AUTO_SELECTED_KEY, uiState.isAutoSelectFlow) + if (biometricState.biometricResult != null) { + if (uiState.isAutoSelectFlow) { + Log.w(Constants.LOG_TAG, "Unexpected biometric result exists when " + + "autoSelect is preferred.") + } + entryIntent?.putExtra(Constants.BIOMETRIC_AUTH_TYPE, + biometricState.biometricResult.biometricAuthenticationResult.authenticationType) + } val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent) .setFillInIntent(entryIntent).build() try { @@ -200,13 +213,20 @@ class CredentialSelectorViewModel( /**************************************************************************/ /***** Get Flow Callbacks *****/ /**************************************************************************/ - fun getFlowOnEntrySelected(entry: EntryInfo) { + fun getFlowOnEntrySelected( + entry: EntryInfo, + authResult: BiometricPrompt.AuthenticationResult? = null + ) { Log.d(Constants.LOG_TAG, "credential selected: {provider=${entry.providerId}" + ", key=${entry.entryKey}, subkey=${entry.entrySubkey}}") uiState = if (entry.pendingIntent != null) { uiState.copy( selectedEntry = entry, providerActivityState = ProviderActivityState.READY_TO_LAUNCH, + biometricState = if (authResult == null) uiState.biometricState else uiState + .biometricState.copy(biometricResult = BiometricResult( + biometricAuthenticationResult = authResult) + ) ) } else { credManRepo.onOptionSelected(entry.providerId, entry.entryKey, entry.entrySubkey) @@ -347,4 +367,9 @@ class CredentialSelectorViewModel( fun logUiEvent(uiEventEnum: UiEventEnum) { this.uiMetrics.log(uiEventEnum, credManRepo.requestInfo?.packageName) } + + companion object { + // TODO(b/326243754) : Replace/remove once all failure flows added in + const val TEMPORARY_FAILURE_CODE = Integer.MIN_VALUE + } }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 6a1998a5e24e..fd6fc6a44c7c 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -503,6 +503,8 @@ class CreateFlowUtils { it.hasHint("androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_" + "SELECT_ALLOWED") }?.text == "true", + // TODO(b/326243754) : Handle this when the create flow is added; for now the + // create flow does not support biometric values ) ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt new file mode 100644 index 000000000000..db5ab569535f --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/BiometricHandler.kt @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.credentialmanager.common + +import android.content.Context +import android.graphics.Bitmap +import android.hardware.biometrics.BiometricManager +import android.hardware.biometrics.BiometricPrompt +import android.os.CancellationSignal +import android.util.Log +import androidx.core.content.ContextCompat.getMainExecutor +import androidx.core.graphics.drawable.toBitmap +import com.android.credentialmanager.R +import com.android.credentialmanager.createflow.EnabledProviderInfo +import com.android.credentialmanager.getflow.ProviderDisplayInfo +import com.android.credentialmanager.getflow.RequestDisplayInfo +import com.android.credentialmanager.getflow.generateDisplayTitleTextResCode +import com.android.credentialmanager.model.BiometricRequestInfo +import com.android.credentialmanager.model.EntryInfo +import com.android.credentialmanager.model.get.CredentialEntryInfo +import com.android.credentialmanager.model.get.ProviderInfo +import java.lang.Exception + +/** + * Aggregates common display information used for the Biometric Flow. + * Namely, this adds the ability to encapsulate the [providerIcon], the providers icon, the + * [providerName], which represents the name of the provider, the [displayTitleText] which is + * the large text displaying the flow in progress, and the [descriptionAboveBiometricButton], which + * describes details of where the credential is being saved, and how. + */ +data class BiometricDisplayInfo( + val providerIcon: Bitmap, + val providerName: String, + val displayTitleText: String, + val descriptionAboveBiometricButton: String, + val biometricRequestInfo: BiometricRequestInfo, +) + +/** + * Sets up generic state used by the create and get flows to hold the holistic states for the flow. + * These match all the present callback values from [BiometricPrompt], and may be extended to hold + * additional states that may improve the flow. + */ +data class BiometricState( + val biometricResult: BiometricResult? = null, + val biometricError: BiometricError? = null, + val biometricHelp: BiometricHelp? = null, + val biometricAcquireInfo: Int? = null, +) + +/** + * When a result exists, it must be retrievable. This encapsulates the result + * so that should this object exist, the result will be retrievable. + */ +data class BiometricResult( + val biometricAuthenticationResult: BiometricPrompt.AuthenticationResult +) + +/** + * Encapsulates the error callback results to easily manage biometric error states in the flow. + */ +data class BiometricError( + val errorCode: Int, + val errString: CharSequence? = null +) + +/** + * Encapsulates the help callback results to easily manage biometric help states in the flow. + * To specify, this allows us to parse the onAuthenticationHelp method in the [BiometricPrompt]. + */ +data class BiometricHelp( + val helpCode: Int, + var helpString: CharSequence? = null +) + +/** + * This will handle the logic for integrating credential manager with the biometric prompt for the + * single account biometric experience. This simultaneously handles both the get and create flows, + * by retrieving all the data from credential manager, and properly parsing that data into the + * biometric prompt. + */ +fun runBiometricFlow( + biometricEntry: EntryInfo, + context: Context, + openMoreOptionsPage: () -> Unit, + sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, + onCancelFlowAndFinish: (String) -> Unit, + getRequestDisplayInfo: RequestDisplayInfo? = null, + getProviderInfoList: List<ProviderInfo>? = null, + getProviderDisplayInfo: ProviderDisplayInfo? = null, + onBiometricFailureFallback: () -> Unit, + createRequestDisplayInfo: com.android.credentialmanager.createflow + .RequestDisplayInfo? = null, + createProviderInfo: EnabledProviderInfo? = null, +) { + var biometricDisplayInfo: BiometricDisplayInfo? = null + if (getRequestDisplayInfo != null) { + biometricDisplayInfo = validateAndRetrieveBiometricGetDisplayInfo(getRequestDisplayInfo, + getProviderInfoList, + getProviderDisplayInfo, + context, biometricEntry) + } else if (createRequestDisplayInfo != null) { + // TODO(b/326243754) : Create Flow to be implemented in follow up + biometricDisplayInfo = validateBiometricCreateFlow( + createRequestDisplayInfo, + createProviderInfo + ) + } + + if (biometricDisplayInfo == null) { + onBiometricFailureFallback() + return + } + + val biometricPrompt = setupBiometricPrompt(context, biometricDisplayInfo, openMoreOptionsPage, + biometricDisplayInfo.biometricRequestInfo.allowedAuthenticators) + + val callback: BiometricPrompt.AuthenticationCallback = + setupBiometricAuthenticationCallback(sendDataToProvider, biometricEntry, + onCancelFlowAndFinish) + + val cancellationSignal = CancellationSignal() + cancellationSignal.setOnCancelListener { + Log.d(TAG, "Your cancellation signal was called.") + // TODO(b/326243754) : Migrate towards passing along the developer cancellation signal + // or validate the necessity for this + } + + val executor = getMainExecutor(context) + + try { + biometricPrompt.authenticate(cancellationSignal, executor, callback) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Calling the biometric prompt API failed with: /n${e.localizedMessage}\n") + onBiometricFailureFallback() + } +} + +/** + * Sets up the biometric prompt with the UI specific bits. + * // TODO(b/326243754) : Pass in opId once dependency is confirmed via CryptoObject + * // TODO(b/326243754) : Given fallbacks aren't allowed, for now we validate that device creds + * // are NOT allowed to be passed in to avoid throwing an error. Later, however, once target + * // alignments occur, we should add the bit back properly. + */ +private fun setupBiometricPrompt( + context: Context, + biometricDisplayInfo: BiometricDisplayInfo, + openMoreOptionsPage: () -> Unit, + requestAllowedAuthenticators: Int, +): BiometricPrompt { + val finalAuthenticators = removeDeviceCredential(requestAllowedAuthenticators) + + val biometricPrompt = BiometricPrompt.Builder(context) + .setTitle(biometricDisplayInfo.displayTitleText) + // TODO(b/326243754) : Migrate to using new methods recently aligned upon + .setNegativeButton(context.getString(R.string + .dropdown_presentation_more_sign_in_options_text), + getMainExecutor(context)) { _, _ -> + openMoreOptionsPage() + } + .setAllowedAuthenticators(finalAuthenticators) + .setConfirmationRequired(true) + // TODO(b/326243754) : Add logo back once new permission privileges sorted out + .setDescription(biometricDisplayInfo.descriptionAboveBiometricButton) + .build() + + return biometricPrompt +} + +// TODO(b/326243754) : Remove after larger level alignments made on fallback negative button +// For the time being, we do not support the pin fallback until UX is decided. +private fun removeDeviceCredential(requestAllowedAuthenticators: Int): Int { + var finalAuthenticators = requestAllowedAuthenticators + + if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL or + BiometricManager.Authenticators.BIOMETRIC_WEAK)) { + finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK + } + + if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL or + BiometricManager.Authenticators.BIOMETRIC_STRONG)) { + finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK + } + + if (requestAllowedAuthenticators == (BiometricManager.Authenticators.DEVICE_CREDENTIAL)) { + finalAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK + } + + return finalAuthenticators +} + +/** + * Sets up the biometric authentication callback. + */ +private fun setupBiometricAuthenticationCallback( + sendDataToProvider: (EntryInfo, BiometricPrompt.AuthenticationResult) -> Unit, + selectedEntry: EntryInfo, + onCancelFlowAndFinish: (String) -> Unit +): BiometricPrompt.AuthenticationCallback { + val callback: BiometricPrompt.AuthenticationCallback = + object : BiometricPrompt.AuthenticationCallback() { + // TODO(b/326243754) : Validate remaining callbacks + override fun onAuthenticationSucceeded( + authResult: BiometricPrompt.AuthenticationResult? + ) { + super.onAuthenticationSucceeded(authResult) + try { + if (authResult != null) { + sendDataToProvider(selectedEntry, authResult) + } else { + onCancelFlowAndFinish("The biometric flow succeeded but unexpectedly " + + "returned a null value.") + // TODO(b/326243754) : Propagate to provider + } + } catch (e: Exception) { + onCancelFlowAndFinish("The biometric flow succeeded but failed on handling " + + "the result. See: \n$e\n") + // TODO(b/326243754) : Propagate to provider + } + } + + override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence?) { + super.onAuthenticationHelp(helpCode, helpString) + Log.d(TAG, "Authentication help discovered: $helpCode and $helpString") + // TODO(b/326243754) : Decide on strategy with provider (a simple log probably + // suffices here) + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence?) { + super.onAuthenticationError(errorCode, errString) + Log.d(TAG, "Authentication error-ed out: $errorCode and $errString") + // TODO(b/326243754) : Propagate to provider + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + Log.d(TAG, "Authentication failed.") + // TODO(b/326243754) : Propagate to provider + } + } + return callback +} + +/** + * Creates the [BiometricDisplayInfo] for get flows, and early handles conditional + * checking between the two. Note that while this method's main purpose is to retrieve the info + * required to display the biometric prompt, it acts as a secondary validator to handle any null + * checks at the beginning of the biometric flow and supports a quick fallback. + * While it's not expected for the flow to be triggered if values are + * missing, some values are by default nullable when they are pulled, such as entries. Thus, this + * acts as a final validation failsafe, without requiring null checks or null forcing around the + * codebase. + */ +private fun validateAndRetrieveBiometricGetDisplayInfo( + getRequestDisplayInfo: RequestDisplayInfo?, + getProviderInfoList: List<ProviderInfo>?, + getProviderDisplayInfo: ProviderDisplayInfo?, + context: Context, + selectedEntry: EntryInfo +): BiometricDisplayInfo? { + if (getRequestDisplayInfo != null && getProviderInfoList != null && + getProviderDisplayInfo != null) { + if (selectedEntry !is CredentialEntryInfo) { return null } + return getBiometricDisplayValues(getProviderInfoList, + context, getRequestDisplayInfo, selectedEntry) + } + return null +} + +/** + * Creates the [BiometricDisplayInfo] for create flows, and early handles conditional + * checking between the two. The reason for this method matches the logic for the + * [validateBiometricGetFlow] with the only difference being that this is for the create flow. + */ +private fun validateBiometricCreateFlow( + createRequestDisplayInfo: com.android.credentialmanager.createflow.RequestDisplayInfo?, + createProviderInfo: EnabledProviderInfo?, +): BiometricDisplayInfo? { + if (createRequestDisplayInfo != null && createProviderInfo != null) { + } else if (createRequestDisplayInfo != null && createProviderInfo != null) { + // TODO(b/326243754) : Create Flow to be implemented in follow up + return createFlowDisplayValues() + } + return null +} + +/** + * Handles the biometric sign in via the 'get credentials' flow. + * If any expected value is not present, the flow is considered unreachable and we will fallback + * to the original selector. Note that these redundant checks are just failsafe; the original + * flow should never reach here with invalid params. + */ +private fun getBiometricDisplayValues( + getProviderInfoList: List<ProviderInfo>, + context: Context, + getRequestDisplayInfo: RequestDisplayInfo, + selectedEntry: CredentialEntryInfo, +): BiometricDisplayInfo? { + var icon: Bitmap? = null + var providerName: String? = null + var displayTitleText: String? = null + var descriptionText: String? = null + val primaryAccountsProviderInfo = retrievePrimaryAccountProviderInfo(selectedEntry.providerId, + getProviderInfoList) + icon = primaryAccountsProviderInfo?.icon?.toBitmap() + providerName = primaryAccountsProviderInfo?.displayName + if (icon == null || providerName == null) { + Log.d(TAG, "Unexpectedly found invalid provider information.") + return null + } + if (selectedEntry.biometricRequest == null) { + Log.d(TAG, "Unexpectedly in biometric flow without a biometric request.") + return null + } + val singleEntryType = selectedEntry.credentialType + val username = selectedEntry.userName + displayTitleText = context.getString( + generateDisplayTitleTextResCode(singleEntryType), + getRequestDisplayInfo.appName + ) + descriptionText = context.getString( + R.string.get_dialog_title_single_tap_for, + getRequestDisplayInfo.appName, + username + ) + return BiometricDisplayInfo(providerIcon = icon, providerName = providerName, + displayTitleText = displayTitleText, descriptionAboveBiometricButton = descriptionText, + biometricRequestInfo = selectedEntry.biometricRequest as BiometricRequestInfo) +} + +/** + * Handles the biometric sign in via the 'create credentials' flow, or early validates this flow + * needs to fallback. + */ +private fun createFlowDisplayValues(): BiometricDisplayInfo? { + // TODO(b/326243754) : Create Flow to be implemented in follow up + return null +} + +/** + * During a get flow with single tap sign in enabled, this will match the credentialEntry that + * will single tap with the correct provider info. Namely, it's the first provider info that + * contains a matching providerId to the selected entry. + */ +private fun retrievePrimaryAccountProviderInfo( + providerId: String, + getProviderInfoList: List<ProviderInfo> +): ProviderInfo? { + var discoveredProviderInfo: ProviderInfo? = null + getProviderInfoList.forEach { provider -> + if (provider.id == providerId) { + discoveredProviderInfo = provider + return@forEach + } + } + return discoveredProviderInfo +} + +const val TAG = "BiometricHandler" diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt index 51ca5971cec4..7e7a74fd3107 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/Constants.kt @@ -22,5 +22,7 @@ class Constants { const val BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS = "androidx.credentials.BUNDLE_KEY_IS_AUTO_SELECT_ALLOWED" const val IS_AUTO_SELECTED_KEY = "IS_AUTO_SELECTED" + const val BIOMETRIC_AUTH_TYPE = "BIOMETRIC_AUTH_TYPE" + const val BIOMETRIC_AUTH_FAILURE = "BIOMETRIC_AUTH_FAILURE" } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt index 99a940968cc5..d13d86fccc97 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -305,10 +305,14 @@ fun CtaButtonRow( modifier = Modifier.fillMaxWidth() ) { if (leftButton != null) { - leftButton() + Box(modifier = Modifier.wrapContentSize().weight(1f, fill = false)) { + leftButton() + } } if (rightButton != null) { - rightButton() + Box(modifier = Modifier.wrapContentSize().weight(1f, fill = false)) { + rightButton() + } } } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt index a46e3586c777..3fb915226963 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt @@ -17,7 +17,6 @@ package com.android.credentialmanager.common.ui import android.content.Context -import android.content.res.Configuration import android.widget.RemoteViews import androidx.core.content.ContextCompat import com.android.credentialmanager.model.get.CredentialEntryInfo @@ -27,10 +26,12 @@ import android.graphics.drawable.Icon class RemoteViewsFactory { companion object { - private const val setAdjustViewBoundsMethodName = "setAdjustViewBounds" - private const val setMaxHeightMethodName = "setMaxHeight" - private const val setBackgroundResourceMethodName = "setBackgroundResource" - private const val bulletPoint = "\u2022" + private const val SET_ADJUST_VIEW_BOUNDS_METHOD_NAME = "setAdjustViewBounds" + private const val SET_MAX_HEIGHT_METHOD_NAME = "setMaxHeight" + private const val SET_BACKGROUND_RESOURCE_METHOD_NAME = "setBackgroundResource" + private const val BULLET_POINT = "\u2022" + // TODO(jbabs): RemoteViews#setViewPadding renders this as 8dp on the display. Debug why. + private const val END_ITEMS_PADDING = 28 fun createDropdownPresentation( context: Context, @@ -50,18 +51,18 @@ class RemoteViewsFactory { val secondaryText = if (credentialEntryInfo.displayName != null && (credentialEntryInfo.displayName != credentialEntryInfo.userName)) - (credentialEntryInfo.userName + " " + bulletPoint + " " + (credentialEntryInfo.userName + " " + BULLET_POINT + " " + credentialEntryInfo.credentialTypeDisplayName - + " " + bulletPoint + " " + credentialEntryInfo.providerDisplayName) - else (credentialEntryInfo.credentialTypeDisplayName + " " + bulletPoint + " " + + " " + BULLET_POINT + " " + credentialEntryInfo.providerDisplayName) + else (credentialEntryInfo.credentialTypeDisplayName + " " + BULLET_POINT + " " + credentialEntryInfo.providerDisplayName) remoteViews.setTextViewText(android.R.id.text2, secondaryText) remoteViews.setImageViewIcon(android.R.id.icon1, icon); remoteViews.setBoolean( - android.R.id.icon1, setAdjustViewBoundsMethodName, true); + android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true); remoteViews.setInt( android.R.id.icon1, - setMaxHeightMethodName, + SET_MAX_HEIGHT_METHOD_NAME, context.resources.getDimensionPixelSize( com.android.credentialmanager.R.dimen.autofill_icon_size)); remoteViews.setContentDescription(android.R.id.icon1, credentialEntryInfo @@ -71,11 +72,11 @@ class RemoteViewsFactory { com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one else com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_middle remoteViews.setInt( - android.R.id.content, setBackgroundResourceMethodName, drawableId); + android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId); if (isFirstEntry) remoteViews.setViewPadding( com.android.credentialmanager.R.id.credential_card, /* left=*/0, - /* top=*/8, + /* top=*/END_ITEMS_PADDING, /* right=*/0, /* bottom=*/0) if (isLastEntry) remoteViews.setViewPadding( @@ -83,7 +84,7 @@ class RemoteViewsFactory { /*left=*/0, /* top=*/0, /* right=*/0, - /* bottom=*/8) + /* bottom=*/END_ITEMS_PADDING) return remoteViews } @@ -95,16 +96,16 @@ class RemoteViewsFactory { com.android.credentialmanager .R.string.dropdown_presentation_more_sign_in_options_text)) remoteViews.setBoolean( - android.R.id.icon1, setAdjustViewBoundsMethodName, true); + android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true); remoteViews.setInt( android.R.id.icon1, - setMaxHeightMethodName, + SET_MAX_HEIGHT_METHOD_NAME, context.resources.getDimensionPixelSize( com.android.credentialmanager.R.dimen.autofill_icon_size)); val drawableId = com.android.credentialmanager.R.drawable.more_options_list_item remoteViews.setInt( - android.R.id.content, setBackgroundResourceMethodName, drawableId); + android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId); return remoteViews } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index b9c9d8994c45..b59ccc264630 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -16,8 +16,10 @@ package com.android.credentialmanager.getflow +import android.credentials.flags.Flags.credmanBiometricApiEnabled import android.credentials.flags.Flags.selectorUiImprovementsEnabled import android.graphics.drawable.Drawable +import android.hardware.biometrics.BiometricPrompt import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.result.ActivityResult import androidx.activity.result.IntentSenderRequest @@ -41,6 +43,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextLayoutResult @@ -49,30 +52,31 @@ import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.R -import com.android.credentialmanager.model.EntryInfo -import com.android.credentialmanager.model.CredentialType -import com.android.credentialmanager.model.get.ProviderInfo import com.android.credentialmanager.common.ProviderActivityState import com.android.credentialmanager.common.material.ModalBottomSheetDefaults +import com.android.credentialmanager.common.runBiometricFlow import com.android.credentialmanager.common.ui.ActionButton import com.android.credentialmanager.common.ui.ActionEntry import com.android.credentialmanager.common.ui.ConfirmButton import com.android.credentialmanager.common.ui.CredentialContainerCard +import com.android.credentialmanager.common.ui.CredentialListSectionHeader import com.android.credentialmanager.common.ui.CtaButtonRow import com.android.credentialmanager.common.ui.Entry +import com.android.credentialmanager.common.ui.HeadlineIcon +import com.android.credentialmanager.common.ui.HeadlineText +import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant import com.android.credentialmanager.common.ui.ModalBottomSheet import com.android.credentialmanager.common.ui.MoreOptionTopAppBar import com.android.credentialmanager.common.ui.SheetContainerCard -import com.android.credentialmanager.common.ui.SnackbarActionText -import com.android.credentialmanager.common.ui.HeadlineText -import com.android.credentialmanager.common.ui.CredentialListSectionHeader -import com.android.credentialmanager.common.ui.HeadlineIcon -import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant import com.android.credentialmanager.common.ui.Snackbar +import com.android.credentialmanager.common.ui.SnackbarActionText import com.android.credentialmanager.logging.GetCredentialEvent +import com.android.credentialmanager.model.CredentialType +import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.get.ActionEntryInfo import com.android.credentialmanager.model.get.AuthenticationEntryInfo import com.android.credentialmanager.model.get.CredentialEntryInfo +import com.android.credentialmanager.model.get.ProviderInfo import com.android.credentialmanager.model.get.RemoteEntryInfo import com.android.credentialmanager.userAndDisplayNameForPasskey import com.android.internal.logging.UiEventLogger.UiEventEnum @@ -82,7 +86,7 @@ import kotlin.math.max fun GetCredentialScreen( viewModel: CredentialSelectorViewModel, getCredentialUiState: GetCredentialUiState, - providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult> + providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>, ) { if (getCredentialUiState.currentScreenState == GetScreenState.REMOTE_ONLY) { RemoteCredentialSnackBarScreen( @@ -137,6 +141,22 @@ fun GetCredentialScreen( } viewModel.uiMetrics.log(GetCredentialEvent .CREDMAN_GET_CRED_SCREEN_PRIMARY_SELECTION) + } else if (credmanBiometricApiEnabled() && getCredentialUiState + .currentScreenState == GetScreenState.BIOMETRIC_SELECTION) { + BiometricSelectionPage( + // TODO(b/326243754) : Utilize expected entry for this flow, confirm + // activeEntry will always be what represents the single tap flow + biometricEntry = getCredentialUiState.activeEntry, + onMoreOptionSelected = viewModel::getFlowOnMoreOptionSelected, + onCancelFlowAndFinish = viewModel::onIllegalUiState, + requestDisplayInfo = getCredentialUiState.requestDisplayInfo, + providerInfoList = getCredentialUiState.providerInfoList, + providerDisplayInfo = getCredentialUiState.providerDisplayInfo, + onBiometricEntrySelected = + viewModel::getFlowOnEntrySelected, + fallbackToOriginalFlow = + viewModel::getFlowOnBackToPrimarySelectionScreen, + ) } else { AllSignInOptionCard( providerInfoList = getCredentialUiState.providerInfoList, @@ -189,6 +209,34 @@ fun GetCredentialScreen( } } +@Composable +internal fun BiometricSelectionPage( + biometricEntry: EntryInfo?, + onCancelFlowAndFinish: (String) -> Unit, + onMoreOptionSelected: () -> Unit, + requestDisplayInfo: RequestDisplayInfo, + providerInfoList: List<ProviderInfo>, + providerDisplayInfo: ProviderDisplayInfo, + onBiometricEntrySelected: (EntryInfo, BiometricPrompt.AuthenticationResult?) -> Unit, + fallbackToOriginalFlow: () -> Unit, +) { + if (biometricEntry == null) { + fallbackToOriginalFlow() + return + } + runBiometricFlow( + biometricEntry = biometricEntry, + context = LocalContext.current, + openMoreOptionsPage = onMoreOptionSelected, + sendDataToProvider = onBiometricEntrySelected, + onCancelFlowAndFinish = onCancelFlowAndFinish, + getRequestDisplayInfo = requestDisplayInfo, + getProviderInfoList = providerInfoList, + getProviderDisplayInfo = providerDisplayInfo, + onBiometricFailureFallback = fallbackToOriginalFlow + ) +} + /** Draws the primary credential selection page, used in Android U. */ // TODO(b/327518384) - remove after flag selectorUiImprovementsEnabled is enabled. @Composable @@ -256,13 +304,8 @@ fun PrimarySelectionCard( if (hasSingleEntry) { val singleEntryType = sortedUserNameToCredentialEntryList.firstOrNull() ?.sortedCredentialEntryList?.firstOrNull()?.credentialType - if (singleEntryType == CredentialType.PASSKEY) - R.string.get_dialog_title_use_passkey_for - else if (singleEntryType == CredentialType.PASSWORD) - R.string.get_dialog_title_use_password_for - else if (authenticationEntryList.isNotEmpty()) - R.string.get_dialog_title_unlock_options_for - else R.string.get_dialog_title_use_sign_in_for + generateDisplayTitleTextResCode(singleEntryType!!, + authenticationEntryList) } else { if (authenticationEntryList.isNotEmpty() || sortedUserNameToCredentialEntryList.any { perNameEntryList -> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index e35acae547a6..6d5b52a7a5f9 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -17,8 +17,11 @@ package com.android.credentialmanager.getflow import android.credentials.flags.Flags.selectorUiImprovementsEnabled +import android.credentials.flags.Flags.credmanBiometricApiEnabled import android.graphics.drawable.Drawable import androidx.credentials.PriorityHints +import com.android.credentialmanager.R +import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.model.get.ProviderInfo import com.android.credentialmanager.model.EntryInfo import com.android.credentialmanager.model.get.AuthenticationEntryInfo @@ -39,6 +42,59 @@ data class GetCredentialUiState( val isNoAccount: Boolean = false, ) +/** + * Checks if this get flow is a biometric selection flow by ensuring that the first account has a + * single credential entry to display. The presently agreed upon condition validates this flow for + * a single account. In the case when there's a single credential, this flow matches the auto + * select criteria, but with the possibility that the two flows (autoselect and biometric) may + * collide. In those collision cases, the auto select flow is supported over the biometric flow. + * If there is a single account but more than one credential, and the first ranked credential has + * the biometric bit flipped on, we will use the biometric flow. If all conditions are valid, this + * responds with the entry utilized by the biometricFlow, or null otherwise. + */ +internal fun findBiometricFlowEntry( + providerDisplayInfo: ProviderDisplayInfo, + isAutoSelectFlow: Boolean +): CredentialEntryInfo? { + if (!credmanBiometricApiEnabled()) { + return null + } + if (isAutoSelectFlow) { + // For this to be true, it must be the case that there is a single entry and a single + // account. If that is the case, and auto-select is enabled along side the one-tap flow, we + // always favor that over the one tap flow. + return null + } + // The flow through an authentication entry, even if only a singular entry exists, is deemed + // as not being eligible for the single tap flow given that it adds any number of credentials + // once unlocked; essentially, this entry contains additional complexities behind it, making it + // invalid. + if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) { + return null + } + val singleAccountEntryList = getCredentialEntryListIffSingleAccount( + providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return null + + val firstEntry = singleAccountEntryList.firstOrNull() + return if (firstEntry?.biometricRequest != null) firstEntry else null +} + +/** + * A utility method that will procure the credential entry list if and only if the credential entry + * list is for a singular account use case. This can be used for various flows that condition on + * a singular account. + */ +internal fun getCredentialEntryListIffSingleAccount( + sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList> +): List<CredentialEntryInfo>? { + if (sortedUserNameToCredentialEntryList.size != 1) { + return null + } + val entryList = sortedUserNameToCredentialEntryList.firstOrNull() ?: return null + val sortedEntryList = entryList.sortedCredentialEntryList + return sortedEntryList +} + internal fun hasContentToDisplay(state: GetCredentialUiState): Boolean { return state.providerDisplayInfo.sortedUserNameToCredentialEntryList.isNotEmpty() || state.providerDisplayInfo.authenticationEntryList.isNotEmpty() || @@ -50,15 +106,14 @@ internal fun findAutoSelectEntry(providerDisplayInfo: ProviderDisplayInfo): Cred if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) { return null } - if (providerDisplayInfo.sortedUserNameToCredentialEntryList.size == 1) { - val entryList = providerDisplayInfo.sortedUserNameToCredentialEntryList.firstOrNull() - ?: return null - if (entryList.sortedCredentialEntryList.size == 1) { - val entry = entryList.sortedCredentialEntryList.firstOrNull() ?: return null - if (entry.isAutoSelectable) { - return entry - } - } + val entryList = getCredentialEntryListIffSingleAccount( + providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return null + if (entryList.size != 1) { + return null + } + val entry = entryList.firstOrNull() ?: return null + if (entry.isAutoSelectable) { + return entry } return null } @@ -105,6 +160,9 @@ enum class GetScreenState { /** The primary credential selection page. */ PRIMARY_SELECTION, + /** The single tap biometric selection page. */ + BIOMETRIC_SELECTION, + /** The secondary credential selection page, where all sign-in options are listed. */ ALL_SIGN_IN_OPTIONS, @@ -177,6 +235,22 @@ fun toProviderDisplayInfo( ) } +/** + * This generates the res code for the large display title text for the selector. For example, it + * retrieves the resource for strings like: "Use your saved passkey for *rpName*". + */ +internal fun generateDisplayTitleTextResCode( + singleEntryType: CredentialType, + authenticationEntryList: List<AuthenticationEntryInfo> = emptyList() +): Int = + if (singleEntryType == CredentialType.PASSKEY) + R.string.get_dialog_title_use_passkey_for + else if (singleEntryType == CredentialType.PASSWORD) + R.string.get_dialog_title_use_password_for + else if (authenticationEntryList.isNotEmpty()) + R.string.get_dialog_title_unlock_options_for + else R.string.get_dialog_title_use_sign_in_for + fun toActiveEntry( providerDisplayInfo: ProviderDisplayInfo, ): EntryInfo? { @@ -211,9 +285,18 @@ private fun toGetScreenState( GetScreenState.REMOTE_ONLY else if (isRequestForAllOptions) GetScreenState.ALL_SIGN_IN_OPTIONS + else if (isBiometricFlow(providerDisplayInfo)) + GetScreenState.BIOMETRIC_SELECTION else GetScreenState.PRIMARY_SELECTION } +/** + * Determines if the flow is a biometric flow by taking into account autoselect criteria. + */ +internal fun isBiometricFlow(providerDisplayInfo: ProviderDisplayInfo) = + findBiometricFlowEntry(providerDisplayInfo, + findAutoSelectEntry(providerDisplayInfo) != null) != null + internal class CredentialEntryInfoComparatorByTypeThenTimestamp( val typePriorityMap: Map<String, Int>, ) : Comparator<CredentialEntryInfo> { diff --git a/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm b/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm index 3f5e8944d977..f2843ed0dd6f 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_azerbaijani.kcm @@ -18,6 +18,8 @@ type OVERLAY +map key 86 PLUS + ### ROW 1 key GRAVE { @@ -42,13 +44,14 @@ key 2 { key 3 { label: '3' base: '3' - shift: '\u2166' + shift: '\u2116' } key 4 { label: '4' base: '4' shift: ';' + ralt: '\u20bc' } key 5 { @@ -61,14 +64,12 @@ key 6 { label: '6' base: '6' shift: ':' - shift+ralt: '^' } key 7 { label: '7' base: '7' shift: '?' - ralt: '&' } key 8 { @@ -176,21 +177,21 @@ key P { key LEFT_BRACKET { label: '\u00d6' base: '\u00f6' - shift: '\u00d6' + shift, capslock: '\u00d6' shift+capslock: '\u00f6' } key RIGHT_BRACKET { label: '\u011e' base: '\u011f' - shift: '\u011e' + shift, capslock: '\u011e' shift+capslock: '\u011f' } key BACKSLASH { label: '\\' base: '\\' - shift: '|' + shift: '/' } ### ROW 3 @@ -261,19 +262,25 @@ key L { key SEMICOLON { label: 'I' base: '\u0131' - shift: 'I' + shift, capslock: 'I' shift+capslock: '\u0131' } key APOSTROPHE { label: '\u018f' base: '\u0259' - shift: '\u018f' + shift, capslock: '\u018f' shift+capslock: '\u0259' } ### ROW 4 +key PLUS { + label: '\\' + base: '\\' + shift: '/' +} + key Z { label: 'Z' base: 'z' @@ -326,14 +333,14 @@ key M { key COMMA { label: '\u00c7' base: '\u00e7' - shift: '\u00c7' + shift, capslock: '\u00c7' shift+capslock: '\u00e7' } key PERIOD { label: '\u015e' base: '\u015f' - shift: '\u015e' + shift, capslock: '\u015e' shift+capslock: '\u015f' } diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm index 071f9f436e04..854c2fdc71ce 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm @@ -23,8 +23,8 @@ map key 43 POUND ### ROW 1 key GRAVE { - label: '`' - base: '`' + label: '\u0300' + base: '\u0300' shift: '\u00AC' ralt: '\u00A6' } @@ -39,6 +39,7 @@ key 2 { label: '2' base: '2' shift: '"' + ralt: '\u0308' } key 3 { @@ -64,6 +65,7 @@ key 6 { label: '6' base: '6' shift: '^' + ralt: '\u0302' } key 7 { @@ -202,6 +204,7 @@ key RIGHT_BRACKET { label: ']' base: ']' shift: '}' + shift+ralt: '|' } ### ROW 3 @@ -282,14 +285,16 @@ key APOSTROPHE { label: '\'' base: '\'' shift: '@' + ralt: '\u0301' + shift+ralt: '`' } key POUND { label: '#' base: '#' shift: '~' - ralt: '\\' - shift+ralt: '|' + ralt: '\u0303' + shift+ralt: '\\' } ### ROW 4 diff --git a/packages/InputDevices/res/raw/keyboard_layout_german.kcm b/packages/InputDevices/res/raw/keyboard_layout_german.kcm index 23ccc9aa6b17..fbb9bb68160b 100644 --- a/packages/InputDevices/res/raw/keyboard_layout_german.kcm +++ b/packages/InputDevices/res/raw/keyboard_layout_german.kcm @@ -101,6 +101,7 @@ key 0 { key SLASH { label: '\u00df' base: '\u00df' + capslock: '\u1e9e' shift: '?' ralt: '\\' } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java index ef418a5c5bde..a4c6ac7d95c7 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java @@ -28,6 +28,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; +import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.net.Uri; @@ -38,7 +39,6 @@ import android.os.UserManager; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.packageinstaller.v2.ui.InstallLaunch; import java.util.Arrays; @@ -51,6 +51,7 @@ public class InstallStart extends Activity { private static final String TAG = InstallStart.class.getSimpleName(); private PackageManager mPackageManager; + private PackageInstaller mPackageInstaller; private UserManager mUserManager; private boolean mAbortInstall = false; private boolean mShouldFinish = true; @@ -66,7 +67,7 @@ public class InstallStart extends Activity { Log.i(TAG, "Using Pia V2"); Intent piaV2 = new Intent(getIntent()); - piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_NAME, getCallingPackage()); + piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_NAME, getLaunchedFromPackage()); piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_UID, getLaunchedFromUid()); piaV2.setClass(this, InstallLaunch.class); piaV2.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); @@ -75,10 +76,11 @@ public class InstallStart extends Activity { return; } mPackageManager = getPackageManager(); + mPackageInstaller = mPackageManager.getPackageInstaller(); mUserManager = getSystemService(UserManager.class); Intent intent = getIntent(); - String callingPackage = getCallingPackage(); + String callingPackage = getLaunchedFromPackage(); String callingAttributionTag = null; // Uid of the source package, coming from ActivityManager @@ -87,31 +89,33 @@ public class InstallStart extends Activity { Log.w(TAG, "Could not determine the launching uid."); } + // The UID of the origin of the installation. Note that it can be different than the + // "installer" of the session. For instance, if a 3P caller launched PIA with an ACTION_VIEW + // intent, the originatingUid is the 3P caller, but the "installer" in this case would + // be PIA. + int originatingUid = callingUid; + final boolean isSessionInstall = PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction()) || PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction()); - // If the activity was started via a PackageInstaller session, we retrieve the calling - // package from that session + // If the activity was started via a PackageInstaller session, we retrieve the originating + // UID from that session final int sessionId = (isSessionInstall - ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1) - : -1); - int originatingUidFromSession = callingUid; - if (callingPackage == null && sessionId != -1) { - PackageInstaller packageInstaller = getPackageManager().getPackageInstaller(); - PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId); + ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID) + : SessionInfo.INVALID_ID); + if (sessionId != SessionInfo.INVALID_ID) { + PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId); if (sessionInfo != null) { - callingPackage = sessionInfo.getInstallerPackageName(); callingAttributionTag = sessionInfo.getInstallerAttributionTag(); - originatingUidFromSession = sessionInfo.getOriginatingUid(); + if (sessionInfo.getOriginatingUid() != Process.INVALID_UID) { + originatingUid = sessionInfo.getOriginatingUid(); + } } } final ApplicationInfo sourceInfo = getSourceInfo(callingPackage); - // Uid of the source package, with a preference to uid from ApplicationInfo - final int originatingUid = sourceInfo != null ? sourceInfo.uid : callingUid; - if (callingUid == Process.INVALID_UID && sourceInfo == null) { Log.e(TAG, "Cannot determine caller since UID is invalid and sourceInfo is null"); mAbortInstall = true; @@ -124,28 +128,28 @@ public class InstallStart extends Activity { boolean isTrustedSource = false; if (sourceInfo != null && sourceInfo.isPrivilegedApp()) { isTrustedSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false) || ( - originatingUid != Process.INVALID_UID && checkPermission( - Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, originatingUid) - == PackageManager.PERMISSION_GRANTED); + callingUid != Process.INVALID_UID && checkPermission( + Manifest.permission.INSTALL_PACKAGES, -1 /* pid */, callingUid) + == PackageManager.PERMISSION_GRANTED); } if (!isTrustedSource && !isSystemDownloadsProvider && !isDocumentsManager - && originatingUid != Process.INVALID_UID) { - final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, originatingUid); + && callingUid != Process.INVALID_UID) { + final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, callingUid); if (targetSdkVersion < 0) { - Log.e(TAG, "Cannot get target sdk version for uid " + originatingUid); + Log.e(TAG, "Cannot get target sdk version for uid " + callingUid); // Invalid originating uid supplied. Abort install. mAbortInstall = true; } else if (targetSdkVersion >= Build.VERSION_CODES.O && !isUidRequestingPermission( - originatingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) { - Log.e(TAG, "Requesting uid " + originatingUid + " needs to declare permission " + callingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) { + Log.e(TAG, "Requesting uid " + callingUid + " needs to declare permission " + Manifest.permission.REQUEST_INSTALL_PACKAGES); mAbortInstall = true; } } - if (sessionId != -1 && !isCallerSessionOwner(originatingUid, sessionId)) { - Log.e(TAG, "UID " + originatingUid + " is not the owner of session " + + if (sessionId != -1 && !isCallerSessionOwner(callingUid, sessionId)) { + Log.e(TAG, "CallingUid " + callingUid + " is not the owner of session " + sessionId); mAbortInstall = true; } @@ -155,10 +159,9 @@ public class InstallStart extends Activity { final String installerPackageNameFromIntent = getIntent().getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME); if (installerPackageNameFromIntent != null) { - final String callingPkgName = getLaunchedFromPackage(); - if (!TextUtils.equals(installerPackageNameFromIntent, callingPkgName) + if (!TextUtils.equals(installerPackageNameFromIntent, callingPackage) && mPackageManager.checkPermission(Manifest.permission.INSTALL_PACKAGES, - callingPkgName) != PackageManager.PERMISSION_GRANTED) { + callingPackage) != PackageManager.PERMISSION_GRANTED) { Log.e(TAG, "The given installer package name " + installerPackageNameFromIntent + " is invalid. Remove it."); EventLog.writeEvent(0x534e4554, "236687884", getLaunchedFromUid(), @@ -186,8 +189,7 @@ public class InstallStart extends Activity { callingAttributionTag); nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo); nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid); - nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINATING_UID_FROM_SESSION_INFO, - originatingUidFromSession); + nextActivity.putExtra(PackageInstallerActivity.EXTRA_IS_TRUSTED_SOURCE, isTrustedSource); if (isSessionInstall) { nextActivity.setClass(this, PackageInstallerActivity.class); @@ -257,7 +259,7 @@ public class InstallStart extends Activity { private ApplicationInfo getSourceInfo(@Nullable String callingPackage) { if (callingPackage != null) { try { - return getPackageManager().getApplicationInfo(callingPackage, 0); + return mPackageManager.getApplicationInfo(callingPackage, 0); } catch (PackageManager.NameNotFoundException ex) { // ignore } @@ -265,8 +267,6 @@ public class InstallStart extends Activity { return null; } - - @NonNull private boolean canPackageQuery(int callingUid, Uri packageUri) { ProviderInfo info = mPackageManager.resolveContentProvider(packageUri.getAuthority(), PackageManager.ComponentInfoFlags.of(0)); @@ -291,17 +291,16 @@ public class InstallStart extends Activity { return false; } - private boolean isCallerSessionOwner(int originatingUid, int sessionId) { - if (originatingUid == Process.ROOT_UID) { + private boolean isCallerSessionOwner(int callingUid, int sessionId) { + if (callingUid == Process.ROOT_UID) { return true; } - PackageInstaller packageInstaller = getPackageManager().getPackageInstaller(); - PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId); + PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId); if (sessionInfo == null) { return false; } int installerUid = sessionInfo.getInstallerUid(); - return originatingUid == installerUid; + return callingUid == installerUid; } private void checkDevicePolicyRestrictions() { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java index 215ead367148..167d50614783 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java @@ -108,18 +108,19 @@ public class InstallSuccess extends Activity { mDialog = builder.create(); mDialog.show(); mDialog.requireViewById(R.id.install_success).setVisibility(View.VISIBLE); - // Enable or disable "launch" button - boolean enabled = false; + // Show or hide "launch" button + boolean visible = false; if (mLaunchIntent != null) { List<ResolveInfo> list = getPackageManager().queryIntentActivities(mLaunchIntent, 0); if (list != null && list.size() > 0) { - enabled = true; + visible = true; } } + visible = visible && isLauncherActivityEnabled(mLaunchIntent); Button launchButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); - if (enabled) { + if (visible) { launchButton.setOnClickListener(view -> { try { startActivity(mLaunchIntent.addFlags( @@ -130,7 +131,15 @@ public class InstallSuccess extends Activity { finish(); }); } else { - launchButton.setEnabled(false); + launchButton.setVisibility(View.GONE); } } + + private boolean isLauncherActivityEnabled(Intent intent) { + if (intent == null || intent.getComponent() == null) { + return false; + } + return getPackageManager().getComponentEnabledSetting(intent.getComponent()) + != PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index 45bfe5469172..8bed945af32c 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -84,8 +84,7 @@ public class PackageInstallerActivity extends Activity { static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO"; static final String EXTRA_STAGED_SESSION_ID = "EXTRA_STAGED_SESSION_ID"; static final String EXTRA_APP_SNIPPET = "EXTRA_APP_SNIPPET"; - static final String EXTRA_ORIGINATING_UID_FROM_SESSION_INFO = - "EXTRA_ORIGINATING_UID_FROM_SESSION_INFO"; + static final String EXTRA_IS_TRUSTED_SOURCE = "EXTRA_IS_TRUSTED_SOURCE"; private static final String ALLOW_UNKNOWN_SOURCES_KEY = PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY"; @@ -98,10 +97,6 @@ public class PackageInstallerActivity extends Activity { * The package name corresponding to #mOriginatingUid */ private String mOriginatingPackage; - /** - * The package name corresponding to the app updater in the update-ownership confirmation dialog - */ - private String mOriginatingPackageFromSessionInfo; private int mActivityResultCode = Activity.RESULT_CANCELED; private int mPendingUserActionReason = -1; @@ -154,8 +149,7 @@ public class PackageInstallerActivity extends Activity { viewToEnable = mDialog.requireViewById(R.id.install_confirm_question_update); final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(); - final CharSequence requestedUpdateOwnerLabel = - getApplicationLabel(mOriginatingPackageFromSessionInfo); + final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mOriginatingPackage); if (!TextUtils.isEmpty(existingUpdateOwnerLabel) && mPendingUserActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP) { String updateOwnerString = @@ -304,21 +298,6 @@ public class PackageInstallerActivity extends Activity { return packagesForUid[0]; } - private boolean isInstallRequestFromUnknownSource(Intent intent) { - if (mCallingPackage != null && intent.getBooleanExtra( - Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) { - if (mSourceInfo != null && mSourceInfo.isPrivilegedApp()) { - // Privileged apps can bypass unknown sources check if they want. - return false; - } - } - if (mSourceInfo != null && checkPermission(Manifest.permission.INSTALL_PACKAGES, - -1 /* pid */, mSourceInfo.uid) == PackageManager.PERMISSION_GRANTED) { - return false; - } - return true; - } - private void initiateInstall() { String pkgName = mPkgInfo.packageName; // Check if there is already a package on the device with this name @@ -384,15 +363,9 @@ public class PackageInstallerActivity extends Activity { mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE); mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG); mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO); - mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, - Process.INVALID_UID); + mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID); mOriginatingPackage = (mOriginatingUid != Process.INVALID_UID) ? getPackageNameForUid(mOriginatingUid) : null; - int originatingUidFromSessionInfo = - intent.getIntExtra(EXTRA_ORIGINATING_UID_FROM_SESSION_INFO, Process.INVALID_UID); - mOriginatingPackageFromSessionInfo = (originatingUidFromSessionInfo != Process.INVALID_UID) - ? getPackageNameForUid(originatingUidFromSessionInfo) : mCallingPackage; - final Object packageSource; if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(action)) { @@ -557,7 +530,7 @@ public class PackageInstallerActivity extends Activity { * Check if it is allowed to install the package and initiate install if allowed. */ private void checkIfAllowedAndInitiateInstall() { - if (mAllowUnknownSources || !isInstallRequestFromUnknownSource(getIntent())) { + if (mAllowUnknownSources || getIntent().getBooleanExtra(EXTRA_IS_TRUSTED_SOURCE, false)) { if (mLocalLOGV) Log.i(TAG, "install allowed"); initiateInstall(); } else { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index aeabbd53d177..08028b1713b8 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -30,6 +30,7 @@ import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller.SessionInfo import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageManager +import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED import android.net.Uri import android.os.ParcelFileDescriptor import android.os.Process @@ -95,6 +96,7 @@ class InstallRepository(private val context: Context) { var stagedSessionId = SessionInfo.INVALID_ID private set private var callingUid = Process.INVALID_UID + private var originatingUid = Process.INVALID_UID private var callingPackage: String? = null private var sessionStager: SessionStager? = null private lateinit var intent: Intent @@ -134,7 +136,7 @@ class InstallRepository(private val context: Context) { callingPackage = callerInfo.packageName - if (callingPackage == null && sessionId != SessionInfo.INVALID_ID) { + if (sessionId != SessionInfo.INVALID_ID) { val sessionInfo: SessionInfo? = packageInstaller.getSessionInfo(sessionId) callingPackage = sessionInfo?.getInstallerPackageName() callingAttributionTag = sessionInfo?.getInstallerAttributionTag() @@ -147,7 +149,7 @@ class InstallRepository(private val context: Context) { } val sourceInfo: ApplicationInfo? = getSourceInfo(callingPackage) // Uid of the source package, with a preference to uid from ApplicationInfo - val originatingUid = sourceInfo?.uid ?: callingUid + originatingUid = sourceInfo?.uid ?: callingUid appOpRequestInfo = AppOpRequestInfo( getPackageNameForUid(context, originatingUid, callingPackage), originatingUid, callingAttributionTag @@ -281,7 +283,7 @@ class InstallRepository(private val context: Context) { context.contentResolver.openAssetFileDescriptor(uri, "r").use { afd -> val pfd: ParcelFileDescriptor? = afd?.parcelFileDescriptor val params: SessionParams = - createSessionParams(intent, pfd, uri.toString()) + createSessionParams(originatingUid, intent, pfd, uri.toString()) stagedSessionId = packageInstaller.createSession(params) } } catch (e: Exception) { @@ -337,6 +339,7 @@ class InstallRepository(private val context: Context) { } private fun createSessionParams( + originatingUid: Int, intent: Intent, pfd: ParcelFileDescriptor?, debugPathName: String, @@ -353,9 +356,7 @@ class InstallRepository(private val context: Context) { params.setOriginatingUri( intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri::class.java) ) - params.setOriginatingUid( - intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID) - ) + params.setOriginatingUid(originatingUid) params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME)) params.setInstallReason(PackageManager.INSTALL_REASON_USER) // Disable full screen intent usage by for sideloads. @@ -830,7 +831,8 @@ class InstallRepository(private val context: Context) { val resultIntent = if (shouldReturnResult) { Intent().putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED) } else { - packageManager.getLaunchIntentForPackage(newPackageInfo!!.packageName) + val intent = packageManager.getLaunchIntentForPackage(newPackageInfo!!.packageName) + if (isLauncherActivityEnabled(intent)) intent else null } _installResult.setValue(InstallSuccess(appSnippet, shouldReturnResult, resultIntent)) } else { @@ -838,6 +840,14 @@ class InstallRepository(private val context: Context) { } } + private fun isLauncherActivityEnabled(intent: Intent?): Boolean { + if (intent == null || intent.component == null) { + return false + } + return (intent.component?.let { packageManager.getComponentEnabledSetting(it) } + != COMPONENT_ENABLED_STATE_DISABLED) + } + /** * Cleanup the staged session. Also signal the packageinstaller that an install session is to * be aborted diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java index b2a65faa0a91..e491f9c87313 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java @@ -23,13 +23,13 @@ import android.content.DialogInterface; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.widget.Button; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.DialogFragment; import com.android.packageinstaller.R; -import com.android.packageinstaller.v2.model.InstallStage; import com.android.packageinstaller.v2.model.InstallSuccess; import com.android.packageinstaller.v2.ui.InstallActionListener; import java.util.List; @@ -40,6 +40,7 @@ import java.util.List; */ public class InstallSuccessFragment extends DialogFragment { + private static final String LOG_TAG = InstallSuccessFragment.class.getSimpleName(); private final InstallSuccess mDialogData; private AlertDialog mDialog; private InstallActionListener mInstallActionListener; @@ -60,12 +61,15 @@ public class InstallSuccessFragment extends DialogFragment { @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null); - mDialog = new AlertDialog.Builder(requireContext()).setTitle(mDialogData.getAppLabel()) - .setIcon(mDialogData.getAppIcon()).setView(dialogView).setNegativeButton(R.string.done, + mDialog = new AlertDialog.Builder(requireContext()) + .setTitle(mDialogData.getAppLabel()) + .setIcon(mDialogData.getAppIcon()) + .setView(dialogView) + .setNegativeButton(R.string.done, (dialog, which) -> mInstallActionListener.onNegativeResponse( - InstallStage.STAGE_SUCCESS)) - .setPositiveButton(R.string.launch, (dialog, which) -> { - }).create(); + mDialogData.getStageCode())) + .setPositiveButton(R.string.launch, (dialog, which) -> {}) + .create(); dialogView.requireViewById(R.id.install_success).setVisibility(View.VISIBLE); @@ -76,25 +80,28 @@ public class InstallSuccessFragment extends DialogFragment { public void onStart() { super.onStart(); Button launchButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE); - boolean enabled = false; + boolean visible = false; if (mDialogData.getResultIntent() != null) { List<ResolveInfo> list = mPm.queryIntentActivities(mDialogData.getResultIntent(), 0); if (list.size() > 0) { - enabled = true; + visible = true; } } - if (enabled) { + if (visible) { launchButton.setOnClickListener(view -> { + Log.i(LOG_TAG, "Finished installing and launching " + + mDialogData.getAppLabel()); mInstallActionListener.openInstalledApp(mDialogData.getResultIntent()); }); } else { - launchButton.setEnabled(false); + launchButton.setVisibility(View.GONE); } } @Override public void onCancel(@NonNull DialogInterface dialog) { super.onCancel(dialog); + Log.i(LOG_TAG, "Finished installing " + mDialogData.getAppLabel()); mInstallActionListener.onNegativeResponse(mDialogData.getStageCode()); } } diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt new file mode 100644 index 000000000000..b52586c2d8d9 --- /dev/null +++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.datastore + +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import java.util.concurrent.Executor +import java.util.concurrent.atomic.AtomicInteger +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class KeyedObserverTest { + @get:Rule + val mockitoRule: MockitoRule = MockitoJUnit.rule() + + @Mock + private lateinit var observer1: KeyedObserver<Any?> + + @Mock + private lateinit var observer2: KeyedObserver<Any?> + + @Mock + private lateinit var keyedObserver1: KeyedObserver<Any> + + @Mock + private lateinit var keyedObserver2: KeyedObserver<Any> + + @Mock + private lateinit var key1: Any + + @Mock + private lateinit var key2: Any + + @Mock + private lateinit var executor: Executor + + private val keyedObservable = KeyedDataObservable<Any>() + + @Test + fun addObserver_sameExecutor() { + keyedObservable.addObserver(observer1, executor) + keyedObservable.addObserver(observer1, executor) + } + + @Test + fun addObserver_keyedObserver_sameExecutor() { + keyedObservable.addObserver(key1, keyedObserver1, executor) + keyedObservable.addObserver(key1, keyedObserver1, executor) + } + + @Test + fun addObserver_differentExecutor() { + keyedObservable.addObserver(observer1, executor) + Assert.assertThrows(IllegalStateException::class.java) { + keyedObservable.addObserver(observer1, directExecutor()) + } + } + + @Test + fun addObserver_keyedObserver_differentExecutor() { + keyedObservable.addObserver(key1, keyedObserver1, executor) + Assert.assertThrows(IllegalStateException::class.java) { + keyedObservable.addObserver(key1, keyedObserver1, directExecutor()) + } + } + + @Test + fun addObserver_weaklyReferenced() { + val counter = AtomicInteger() + var observer: KeyedObserver<Any?>? = KeyedObserver { _, _ -> counter.incrementAndGet() } + keyedObservable.addObserver(observer!!, directExecutor()) + + keyedObservable.notifyChange(ChangeReason.UPDATE) + assertThat(counter.get()).isEqualTo(1) + + // trigger GC, the observer callback should not be invoked + null.also { observer = it } + System.gc() + System.runFinalization() + + keyedObservable.notifyChange(ChangeReason.UPDATE) + assertThat(counter.get()).isEqualTo(1) + } + + @Test + fun addObserver_keyedObserver_weaklyReferenced() { + val counter = AtomicInteger() + var keyObserver: KeyedObserver<Any>? = KeyedObserver { _, _ -> counter.incrementAndGet() } + keyedObservable.addObserver(key1, keyObserver!!, directExecutor()) + + keyedObservable.notifyChange(key1, ChangeReason.UPDATE) + assertThat(counter.get()).isEqualTo(1) + + // trigger GC, the observer callback should not be invoked + null.also { keyObserver = it } + System.gc() + System.runFinalization() + + keyedObservable.notifyChange(key1, ChangeReason.UPDATE) + assertThat(counter.get()).isEqualTo(1) + } + + @Test + fun addObserver_notifyObservers_removeObserver() { + keyedObservable.addObserver(observer1, directExecutor()) + keyedObservable.addObserver(observer2, executor) + + keyedObservable.notifyChange(ChangeReason.UPDATE) + verify(observer1).onKeyChanged(null, ChangeReason.UPDATE) + verify(observer2, never()).onKeyChanged(any(), any()) + verify(executor).execute(any()) + + reset(observer1, executor) + keyedObservable.removeObserver(observer2) + + keyedObservable.notifyChange(ChangeReason.DELETE) + verify(observer1).onKeyChanged(null, ChangeReason.DELETE) + verify(executor, never()).execute(any()) + } + + @Test + fun addObserver_keyedObserver_notifyObservers_removeObserver() { + keyedObservable.addObserver(key1, keyedObserver1, directExecutor()) + keyedObservable.addObserver(key2, keyedObserver2, executor) + + keyedObservable.notifyChange(key1, ChangeReason.UPDATE) + verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE) + verify(keyedObserver2, never()).onKeyChanged(any(), any()) + verify(executor, never()).execute(any()) + + reset(keyedObserver1, executor) + keyedObservable.removeObserver(key2, keyedObserver2) + + keyedObservable.notifyChange(key1, ChangeReason.DELETE) + verify(keyedObserver1).onKeyChanged(key1, ChangeReason.DELETE) + verify(executor, never()).execute(any()) + } + + @Test + fun notifyChange_addMoreTypeObservers_checkOnKeyChanged() { + keyedObservable.addObserver(observer1, directExecutor()) + keyedObservable.addObserver(key1, keyedObserver1, directExecutor()) + keyedObservable.addObserver(key2, keyedObserver2, directExecutor()) + + keyedObservable.notifyChange(ChangeReason.UPDATE) + verify(observer1).onKeyChanged(null, ChangeReason.UPDATE) + verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE) + verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE) + + reset(observer1, keyedObserver1, keyedObserver2) + keyedObservable.notifyChange(key1, ChangeReason.UPDATE) + + verify(observer1).onKeyChanged(key1, ChangeReason.UPDATE) + verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE) + verify(keyedObserver2, never()).onKeyChanged(key1, ChangeReason.UPDATE) + + reset(observer1, keyedObserver1, keyedObserver2) + keyedObservable.notifyChange(key2, ChangeReason.UPDATE) + + verify(observer1).onKeyChanged(key2, ChangeReason.UPDATE) + verify(keyedObserver1, never()).onKeyChanged(key2, ChangeReason.UPDATE) + verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE) + } + + @Test + fun notifyChange_addObserverWithinCallback() { + // ConcurrentModificationException is raised if it is not implemented correctly + val observer: KeyedObserver<Any?> = KeyedObserver { _, _ -> + keyedObservable.addObserver(observer1, executor) + } + + keyedObservable.addObserver(observer, directExecutor()) + + keyedObservable.notifyChange(ChangeReason.UPDATE) + keyedObservable.removeObserver(observer) + } + + @Test + fun notifyChange_KeyedObserver_addObserverWithinCallback() { + // ConcurrentModificationException is raised if it is not implemented correctly + val keyObserver: KeyedObserver<Any?> = KeyedObserver { _, _ -> + keyedObservable.addObserver(key1, keyedObserver1, executor) + } + + keyedObservable.addObserver(key1, keyObserver, directExecutor()) + + keyedObservable.notifyChange(key1, ChangeReason.UPDATE) + keyedObservable.removeObserver(key1, keyObserver) + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt index bb791dc9a23c..f0658290beb0 100644 --- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt +++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt @@ -69,8 +69,7 @@ class ObserverTest { assertThat(counter.get()).isEqualTo(1) // trigger GC, the observer callback should not be invoked - @Suppress("unused") - observer = null + null.also { observer = it } System.gc() System.runFinalization() @@ -100,10 +99,12 @@ class ObserverTest { @Test fun notifyChange_addObserverWithinCallback() { // ConcurrentModificationException is raised if it is not implemented correctly + val observer = Observer { observable.addObserver(observer1, executor) } observable.addObserver( - { observable.addObserver(observer1, executor) }, + observer, MoreExecutors.directExecutor() ) observable.notifyChange(ChangeReason.UPDATE) + observable.removeObserver(observer) } } diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp index 6dc07b29a510..4aa67c17ad98 100644 --- a/packages/SettingsLib/ProfileSelector/Android.bp +++ b/packages/SettingsLib/ProfileSelector/Android.bp @@ -20,6 +20,7 @@ android_library { static_libs: [ "com.google.android.material_material", "SettingsLibSettingsTheme", + "android.os.flags-aconfig-java-export", ], sdk_version: "system_current", diff --git a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml index 80f6b7683269..303e20c2497e 100644 --- a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml +++ b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml @@ -18,5 +18,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.widget.profileselector"> - <uses-sdk android:minSdkVersion="23" /> + <uses-sdk android:minSdkVersion="29" /> </manifest> diff --git a/packages/SettingsLib/ProfileSelector/res/values/strings.xml b/packages/SettingsLib/ProfileSelector/res/values/strings.xml index 68d4047a497c..76ccb651969b 100644 --- a/packages/SettingsLib/ProfileSelector/res/values/strings.xml +++ b/packages/SettingsLib/ProfileSelector/res/values/strings.xml @@ -21,4 +21,6 @@ <string name="settingslib_category_personal">Personal</string> <!-- Header for items under the work user [CHAR LIMIT=30] --> <string name="settingslib_category_work">Work</string> + <!-- Header for items under the private profile user [CHAR LIMIT=30] --> + <string name="settingslib_category_private">Private</string> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java index be5753beea4e..c52386bef07b 100644 --- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java +++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java @@ -16,31 +16,77 @@ package com.android.settingslib.widget; +import android.annotation.TargetApi; import android.app.Activity; +import android.content.Context; +import android.content.pm.UserProperties; +import android.os.Build; import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.ArrayMap; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.core.os.BuildCompat; import androidx.fragment.app.Fragment; import androidx.viewpager2.widget.ViewPager2; +import com.android.settingslib.widget.profileselector.R; + import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; -import com.android.settingslib.widget.profileselector.R; + +import java.util.ArrayList; +import java.util.List; /** * Base fragment class for profile settings. */ public abstract class ProfileSelectFragment extends Fragment { + private static final String TAG = "ProfileSelectFragment"; + // UserHandle#USER_NULL is a @TestApi so is not accessible. + private static final int USER_NULL = -10000; + private static final int DEFAULT_POSITION = 0; + + /** + * The type of profile tab of {@link ProfileSelectFragment} to show + * <ul> + * <li>0: Personal tab. + * <li>1: Work profile tab. + * </ul> + * + * <p> Please note that this is supported for legacy reasons. Please use + * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} instead. + */ + public static final String EXTRA_SHOW_FRAGMENT_TAB = ":settings:show_fragment_tab"; + + /** + * An {@link ArrayList} of users to show. The supported users are: System user, the managed + * profile user, and the private profile user. A client should pass all the user ids that need + * to be shown in this list. Note that if this list is not provided then, for legacy reasons + * see {@link #EXTRA_SHOW_FRAGMENT_TAB}, an attempt will be made to show two tabs: one for the + * System user and one for the managed profile user. + * + * <p>Please note that this MUST be used in conjunction with + * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} + */ + public static final String EXTRA_LIST_OF_USER_IDS = ":settings:list_user_ids"; /** - * Personal or Work profile tab of {@link ProfileSelectFragment} - * <p>0: Personal tab. - * <p>1: Work profile tab. + * The user id of the user to be show in {@link ProfileSelectFragment}. Only the below user + * types are supported: + * <ul> + * <li> System user. + * <li> Managed profile user. + * <li> Private profile user. + * </ul> + * + * <p>Please note that this MUST be used in conjunction with {@link #EXTRA_LIST_OF_USER_IDS}. */ - public static final String EXTRA_SHOW_FRAGMENT_TAB = - ":settings:show_fragment_tab"; + public static final String EXTRA_SHOW_FRAGMENT_USER_ID = ":settings:show_fragment_user_id"; /** * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB @@ -48,13 +94,23 @@ public abstract class ProfileSelectFragment extends Fragment { public static final int PERSONAL_TAB = 0; /** - * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB + * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB for the managed profile */ public static final int WORK_TAB = 1; + /** + * Please note that private profile is available from API LEVEL + * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} only, therefore PRIVATE_TAB MUST be + * passed in {@link #EXTRA_SHOW_FRAGMENT_TAB} and {@link #EXTRA_LIST_OF_PROFILE_TABS} for + * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher API Levels only. + */ + private static final int PRIVATE_TAB = 2; + private ViewGroup mContentView; private ViewPager2 mViewPager; + private final ArrayMap<UserHandle, Integer> mProfileTabsByUsers = new ArrayMap<>(); + private boolean mUsingUserIds = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -67,7 +123,7 @@ public abstract class ProfileSelectFragment extends Fragment { if (titleResId > 0) { activity.setTitle(titleResId); } - final int selectedTab = getTabId(activity, getArguments()); + initProfileTabsToShow(); final View tabContainer = mContentView.findViewById(R.id.tab_container); mViewPager = tabContainer.findViewById(R.id.view_pager); @@ -78,16 +134,14 @@ public abstract class ProfileSelectFragment extends Fragment { ).attach(); tabContainer.setVisibility(View.VISIBLE); - final TabLayout.Tab tab = tabs.getTabAt(selectedTab); + final TabLayout.Tab tab = tabs.getTabAt(getSelectedTabPosition(activity, getArguments())); tab.select(); return mContentView; } /** - * create Personal or Work profile fragment - * <p>0: Personal profile. - * <p>1: Work profile. + * Create Personal or Work or Private profile fragment. See {@link #EXTRA_SHOW_FRAGMENT_USER_ID} */ public abstract Fragment createFragment(int position); @@ -99,21 +153,90 @@ public abstract class ProfileSelectFragment extends Fragment { return 0; } - int getTabId(Activity activity, Bundle bundle) { + int getSelectedTabPosition(Activity activity, Bundle bundle) { if (bundle != null) { + final int extraUserId = bundle.getInt(EXTRA_SHOW_FRAGMENT_USER_ID, USER_NULL); + if (extraUserId != USER_NULL) { + return mProfileTabsByUsers.indexOfKey(UserHandle.of(extraUserId)); + } final int extraTab = bundle.getInt(EXTRA_SHOW_FRAGMENT_TAB, -1); if (extraTab != -1) { return extraTab; } } - return PERSONAL_TAB; + return DEFAULT_POSITION; + } + + int getTabCount() { + return mUsingUserIds ? mProfileTabsByUsers.size() : 2; + } + + void initProfileTabsToShow() { + Bundle bundle = getArguments(); + if (bundle != null) { + ArrayList<Integer> userIdsToShow = + bundle.getIntegerArrayList(EXTRA_LIST_OF_USER_IDS); + if (userIdsToShow != null && !userIdsToShow.isEmpty()) { + mUsingUserIds = true; + UserManager userManager = getContext().getSystemService(UserManager.class); + List<UserHandle> userHandles = userManager.getUserProfiles(); + for (UserHandle userHandle : userHandles) { + if (!userIdsToShow.contains(userHandle.getIdentifier())) { + continue; + } + if (userHandle.isSystem()) { + mProfileTabsByUsers.put(userHandle, PERSONAL_TAB); + } else if (userManager.isManagedProfile(userHandle.getIdentifier())) { + mProfileTabsByUsers.put(userHandle, WORK_TAB); + } else if (shouldShowPrivateProfileIfItsOne(userHandle)) { + mProfileTabsByUsers.put(userHandle, PRIVATE_TAB); + } + } + } + } + } + + private int getProfileTabForPosition(int position) { + return mUsingUserIds ? mProfileTabsByUsers.valueAt(position) : position; + } + + int getUserIdForPosition(int position) { + return mUsingUserIds ? mProfileTabsByUsers.keyAt(position).getIdentifier() : position; } private CharSequence getPageTitle(int position) { - if (position == WORK_TAB) { + int tab = getProfileTabForPosition(position); + if (tab == WORK_TAB) { return getContext().getString(R.string.settingslib_category_work); + } else if (tab == PRIVATE_TAB) { + return getContext().getString(R.string.settingslib_category_private); } return getString(R.string.settingslib_category_personal); } + + @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private boolean shouldShowUserInQuietMode(UserHandle userHandle, UserManager userManager) { + UserProperties userProperties = userManager.getUserProperties(userHandle); + return !userManager.isQuietModeEnabled(userHandle) + || userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; + } + + // It's sufficient to have this method marked with the appropriate API level because we expect + // to be here only for this API level - when then private profile was introduced. + @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private boolean shouldShowPrivateProfileIfItsOne(UserHandle userHandle) { + if (!BuildCompat.isAtLeastV() || !android.os.Flags.allowPrivateProfile()) { + return false; + } + try { + Context userContext = getContext().createContextAsUser(userHandle, /* flags= */ 0); + UserManager userManager = userContext.getSystemService(UserManager.class); + return userManager.isPrivateProfile() + && shouldShowUserInQuietMode(userHandle, userManager); + } catch (IllegalStateException exception) { + Log.i(TAG, "Ignoring this user as the calling package not available in this user."); + } + return false; + } } diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java index f5ab64742992..37f4f275cfe7 100644 --- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java +++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java @@ -18,7 +18,6 @@ package com.android.settingslib.widget; import androidx.fragment.app.Fragment; import androidx.viewpager2.adapter.FragmentStateAdapter; -import com.android.settingslib.widget.profileselector.R; /** * ViewPager Adapter to handle between TabLayout and ViewPager2 @@ -34,11 +33,11 @@ public class ProfileViewPagerAdapter extends FragmentStateAdapter { @Override public Fragment createFragment(int position) { - return mParentFragments.createFragment(position); + return mParentFragments.createFragment(mParentFragments.getUserIdForPosition(position)); } @Override public int getItemCount() { - return 2; + return mParentFragments.getTabCount(); } } diff --git a/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml index e3645b55981b..beed90efb508 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-night-v34/colors.xml @@ -37,4 +37,8 @@ <!-- Material next track off color--> <color name="settingslib_track_off_color">@android:color/system_surface_container_highest_dark </color> + + <color name="settingslib_text_color_primary_device_default">@android:color/system_on_surface_dark</color> + + <color name="settingslib_text_color_secondary_device_default">@android:color/system_on_surface_variant_dark</color> </resources> diff --git a/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml b/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml index fdd96ec78efc..3709b5d13056 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v34/colors.xml @@ -39,4 +39,8 @@ <!-- Material next track outline color--> <color name="settingslib_track_online_color">@color/settingslib_switch_track_outline_color</color> + + <color name="settingslib_text_color_primary_device_default">@android:color/system_on_surface_light</color> + + <color name="settingslib_text_color_secondary_device_default">@android:color/system_on_surface_variant_light</color> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt index 5dfecb0b7bd4..87cd2b844a2b 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt @@ -70,26 +70,26 @@ internal class RestrictedSwitchPreferenceModel( is BlockedByAdmin -> { Box( Modifier - .clickable( - role = Role.Switch, - onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() }, - ) - .semantics { - this.toggleableState = ToggleableState(checked()) - }, + .clickable( + role = Role.Switch, + onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() }, + ) + .semantics { + this.toggleableState = ToggleableState(checked()) + }, ) { content() } } is BlockedByEcm -> { Box( Modifier - .clickable( - role = Role.Switch, - onClick = { restrictedMode.showRestrictedSettingsDetails() }, - ) - .semantics { - this.toggleableState = ToggleableState(checked()) - }, + .clickable( + role = Role.Switch, + onClick = { restrictedMode.showRestrictedSettingsDetails() }, + ) + .semantics { + this.toggleableState = ToggleableState(checked()) + }, ) { content() } } @@ -113,7 +113,7 @@ internal class RestrictedSwitchPreferenceModel( content: @Composable (SwitchPreferenceModel) -> Unit, ) { val context = LocalContext.current - val restrictedSwitchPreferenceModel = remember(restrictedMode) { + val restrictedSwitchPreferenceModel = remember(restrictedMode, model.title) { RestrictedSwitchPreferenceModel(context, model, restrictedMode) } restrictedSwitchPreferenceModel.RestrictionWrapper { diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt index f812f959db32..5a6c0a1bf275 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt @@ -27,9 +27,8 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.test.junit4.createComposeRule import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spa.testutils.delay import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -72,12 +71,13 @@ class DisposableBroadcastReceiverAsUserTest { DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {} } } + composeTestRule.delay() assertThat(registeredBroadcastReceiver).isNotNull() } @Test - fun broadcastReceiver_isCalledOnReceive() = runBlocking { + fun broadcastReceiver_isCalledOnReceive() { var onReceiveIsCalled = false composeTestRule.setContent { CompositionLocalProvider( @@ -91,7 +91,7 @@ class DisposableBroadcastReceiverAsUserTest { } registeredBroadcastReceiver!!.onReceive(context, Intent()) - delay(100) + composeTestRule.delay() assertThat(onReceiveIsCalled).isTrue() } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt index 2e6a39603deb..c1d298d0b613 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalChangeFlowTest.kt @@ -48,18 +48,7 @@ class SettingsGlobalChangeFlowTest { } @Test - fun settingsGlobalChangeFlow_collectAfterValueChanged_onlyKeepLatest() = runBlocking { - var value by context.settingsGlobalBoolean(TEST_NAME) - value = false - - val flow = context.settingsGlobalChangeFlow(TEST_NAME) - value = true - - assertThat(flow.toListWithTimeout()).hasSize(1) - } - - @Test - fun settingsGlobalChangeFlow_collectBeforeValueChanged_getBoth() = runBlocking { + fun settingsGlobalChangeFlow_changed() = runBlocking { var value by context.settingsGlobalBoolean(TEST_NAME) value = false @@ -69,7 +58,7 @@ class SettingsGlobalChangeFlowTest { delay(100) value = true - assertThat(listDeferred.await()).hasSize(2) + assertThat(listDeferred.await().size).isAtLeast(2) } private companion object { diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index 87b4c0f4230d..ad0e6f46d9c8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -27,6 +27,7 @@ import android.hardware.usb.UsbManager; import android.hardware.usb.UsbPort; import android.hardware.usb.UsbPortStatus; import android.hardware.usb.flags.Flags; +import android.icu.text.NumberFormat; import android.location.LocationManager; import android.media.AudioManager; import android.net.NetworkCapabilities; @@ -41,7 +42,6 @@ import android.os.UserHandle; import android.os.UserManager; import android.print.PrintManager; import android.provider.Settings; -import android.provider.Settings.Secure; import android.telephony.AccessNetworkConstants; import android.telephony.NetworkRegistrationInfo; import android.telephony.ServiceState; @@ -67,7 +67,6 @@ import com.android.settingslib.drawable.UserIconDrawable; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.settingslib.utils.BuildCompatUtils; -import java.text.NumberFormat; import java.time.Duration; import java.util.List; @@ -784,29 +783,4 @@ public class Utils { } return false; } - - /** Whether to show the wireless charging warning in Settings. */ - public static boolean shouldShowWirelessChargingWarningTip( - @NonNull Context context, @NonNull String tag) { - try { - return Secure.getInt(context.getContentResolver(), WIRELESS_CHARGING_WARNING_ENABLED, 0) - == 1; - } catch (Exception e) { - Log.e(tag, "shouldShowWirelessChargingWarningTip()", e); - } - return false; - } - - /** Stores the state of whether the wireless charging warning in Settings is enabled. */ - public static void updateWirelessChargingWarningEnabled( - @NonNull Context context, boolean enabled, @NonNull String tag) { - try { - Secure.putInt( - context.getContentResolver(), - WIRELESS_CHARGING_WARNING_ENABLED, - enabled ? 1 : 0); - } catch (Exception e) { - Log.e(tag, "setWirelessChargingWarningEnabled()", e); - } - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java b/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java index 9a29f2250b7e..f73081a4eb60 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/RecentAppOpsAccess.java @@ -23,6 +23,7 @@ import android.content.PermissionChecker; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.UserProperties; import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.os.UserManager; @@ -132,8 +133,9 @@ public class RecentAppOpsAccess { int uid = ops.getUid(); UserHandle user = UserHandle.getUserHandleForUid(uid); - // Don't show apps belonging to background users except managed users. - if (!profiles.contains(user)) { + // Don't show apps belonging to background users except for profiles that shouldn't + // be shown in quiet mode. + if (!profiles.contains(user) || isHideInQuietEnabledForProfile(um, user)) { continue; } @@ -192,6 +194,16 @@ public class RecentAppOpsAccess { return accesses; } + private boolean isHideInQuietEnabledForProfile(UserManager userManager, UserHandle userHandle) { + if (android.multiuser.Flags.enablePrivateSpaceFeatures() + && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) { + return userManager.isQuietModeEnabled(userHandle) + && userManager.getUserProperties(userHandle).getShowInQuietMode() + == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; + } + return false; + } + /** * Creates a Access entry for the given PackageOps. * diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java index 416b36981a4c..baccda7e3cc4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java @@ -163,6 +163,16 @@ public interface BluetoothCallback { default void onAclConnectionStateChanged( @NonNull CachedBluetoothDevice cachedDevice, int state) {} + /** + * Called when the Auto-on state is changed for any user. Listens to intent + * {@link android.bluetooth.BluetoothAdapter#ACTION_AUTO_ON_STATE_CHANGED } + * + * @param state the Auto-on state, the possible values are: + * {@link android.bluetooth.BluetoothAdapter#AUTO_ON_STATE_ENABLED}, + * {@link android.bluetooth.BluetoothAdapter#AUTO_ON_STATE_DISABLED} + */ + default void onAutoOnStateChanged(int state) {} + @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = { "STATE_" }, value = { STATE_DISCONNECTED, diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index 647fcb9f67fa..0996d52b0e30 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java @@ -133,6 +133,8 @@ public class BluetoothEventManager { addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler()); addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler()); + addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler()); + registerAdapterIntentReceiver(); } @@ -552,4 +554,21 @@ public class BluetoothEventManager { dispatchAudioModeChanged(); } } + + private class AutoOnStateChangedHandler implements Handler { + + @Override + public void onReceive(Context context, Intent intent, BluetoothDevice device) { + String action = intent.getAction(); + if (action == null) { + Log.w(TAG, "AutoOnStateChangedHandler() action is null"); + return; + } + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_AUTO_ON_STATE, + BluetoothAdapter.ERROR); + for (BluetoothCallback callback : mCallbacks) { + callback.onAutoOnStateChanged(state); + } + } + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index b8624fd9605b..4777b0de0732 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -1315,8 +1315,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> boolean isActiveAshaHearingAid = mIsActiveDeviceHearingAid; boolean isActiveLeAudioHearingAid = mIsActiveDeviceLeAudio && isConnectedHapClientDevice(); - if ((isActiveAshaHearingAid || isActiveLeAudioHearingAid) - && stringRes == R.string.bluetooth_active_no_battery_level) { + if (isActiveAshaHearingAid || isActiveLeAudioHearingAid) { final Set<CachedBluetoothDevice> memberDevices = getMemberDevice(); final CachedBluetoothDevice subDevice = getSubDevice(); if (memberDevices.stream().anyMatch(m -> m.isConnected())) { diff --git a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt index cda6b8bb36be..68f471dd4e4f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt +++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt @@ -17,6 +17,7 @@ package com.android.settingslib.media.session import android.media.session.MediaController +import android.media.session.MediaSession import android.media.session.MediaSessionManager import android.os.UserHandle import androidx.concurrent.futures.DirectExecutor @@ -28,7 +29,7 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.launch /** [Flow] for [MediaSessionManager.OnActiveSessionsChangedListener]. */ -val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?> +val MediaSessionManager.activeMediaChanges: Flow<List<MediaController>?> get() = callbackFlow { val listener = @@ -42,3 +43,24 @@ val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?> awaitClose { removeOnActiveSessionsChangedListener(listener) } } .buffer(capacity = Channel.CONFLATED) + +/** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */ +val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?> + get() = + callbackFlow { + val callback = + object : MediaSessionManager.RemoteSessionCallback { + override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) { + launch { send(sessionToken) } + } + + override fun onDefaultRemoteSessionChanged( + sessionToken: MediaSession.Token? + ) { + launch { send(sessionToken) } + } + } + registerRemoteSessionCallback(DirectExecutor.INSTANCE, callback) + awaitClose { unregisterRemoteSessionCallback(callback) } + } + .buffer(capacity = Channel.CONFLATED) diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt index 6730aadbdeb3..e7fec692bd63 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt @@ -19,7 +19,6 @@ package com.android.settingslib.volume.data.repository import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.AudioManager.OnCommunicationDeviceChangedListener -import androidx.concurrent.futures.DirectExecutor import com.android.internal.util.ConcurrentUtils import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.settingslib.volume.shared.model.AudioManagerEvent @@ -109,8 +108,8 @@ class AudioRepositoryImpl( callbackFlow { val listener = OnCommunicationDeviceChangedListener { trySend(Unit) } audioManager.addOnCommunicationDeviceChangedListener( - DirectExecutor.INSTANCE, - listener + ConcurrentUtils.DIRECT_EXECUTOR, + listener, ) awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) } @@ -146,7 +145,7 @@ class AudioRepositoryImpl( maxVolume = audioManager.getStreamMaxVolume(audioStream.value), volume = audioManager.getStreamVolume(audioStream.value), isAffectedByRingerMode = audioManager.isStreamAffectedByRingerMode(audioStream.value), - isMuted = audioManager.isStreamMute(audioStream.value), + isMuted = audioManager.isStreamMute(audioStream.value) ) } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt index 298dd71e555e..724dd51b8fe4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt @@ -15,14 +15,10 @@ */ package com.android.settingslib.volume.data.repository -import android.media.MediaRouter2Manager -import android.media.RoutingSessionInfo import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice -import com.android.settingslib.volume.data.model.RoutingSession import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.settingslib.volume.shared.model.AudioManagerEvent -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -30,35 +26,23 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext /** Repository providing data about connected media devices. */ interface LocalMediaRepository { - /** Available devices list */ - val mediaDevices: StateFlow<Collection<MediaDevice>> - /** Currently connected media device */ val currentConnectedDevice: StateFlow<MediaDevice?> - - val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> - - suspend fun adjustSessionVolume(sessionId: String?, volume: Int) } class LocalMediaRepositoryImpl( audioManagerEventsReceiver: AudioManagerEventsReceiver, private val localMediaManager: LocalMediaManager, - private val mediaRouter2Manager: MediaRouter2Manager, coroutineScope: CoroutineScope, - private val backgroundContext: CoroutineContext, ) : LocalMediaRepository { private val devicesChanges = @@ -94,18 +78,6 @@ class LocalMediaRepositoryImpl( } .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0) - override val mediaDevices: StateFlow<Collection<MediaDevice>> = - mediaDevicesUpdates - .mapNotNull { - if (it is DevicesUpdate.DeviceListUpdate) { - it.newDevices ?: emptyList() - } else { - null - } - } - .flowOn(backgroundContext) - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) - override val currentConnectedDevice: StateFlow<MediaDevice?> = merge(devicesChanges, mediaDevicesUpdates) .map { localMediaManager.currentConnectedDevice } @@ -116,30 +88,6 @@ class LocalMediaRepositoryImpl( localMediaManager.currentConnectedDevice ) - override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> = - merge(devicesChanges, mediaDevicesUpdates) - .onStart { emit(Unit) } - .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) } - .flowOn(backgroundContext) - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) - - override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) { - withContext(backgroundContext) { - if (sessionId == null) { - localMediaManager.adjustSessionVolume(volume) - } else { - localMediaManager.adjustSessionVolume(sessionId, volume) - } - } - } - - private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession = - RoutingSession( - info, - isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(), - isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info) - ) - private sealed interface DevicesUpdate { data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt index 7c231d1fad4e..e4ac9fe686a3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt @@ -27,18 +27,26 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** Provides controllers for currently active device media sessions. */ interface MediaControllerRepository { - /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */ - val activeLocalMediaController: StateFlow<MediaController?> + /** + * Get a list of controllers for all ongoing sessions. The controllers will be provided in + * priority order with the most important controller at index 0. + * + * This requires the [android.Manifest.permission.MEDIA_CONTENT_CONTROL] permission be held by + * the calling app. + */ + val activeSessions: StateFlow<List<MediaController>> } class MediaControllerRepositoryImpl( @@ -49,51 +57,17 @@ class MediaControllerRepositoryImpl( backgroundContext: CoroutineContext, ) : MediaControllerRepository { - private val devicesChanges = - audioManagerEventsReceiver.events.filterIsInstance( - AudioManagerEvent.StreamDevicesChanged::class - ) - - override val activeLocalMediaController: StateFlow<MediaController?> = - combine( - mediaSessionManager.activeMediaChanges.onStart { - emit(mediaSessionManager.getActiveSessions(null)) - }, - localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) } - ?: flowOf(null), - devicesChanges.onStart { emit(AudioManagerEvent.StreamDevicesChanged) }, - ) { controllers, _, _ -> - controllers?.let(::findLocalMediaController) - } + override val activeSessions: StateFlow<List<MediaController>> = + merge( + mediaSessionManager.activeMediaChanges.filterNotNull(), + localBluetoothManager?.headsetAudioModeChanges?.map { + mediaSessionManager.getActiveSessions(null) + } ?: emptyFlow(), + audioManagerEventsReceiver.events + .filterIsInstance(AudioManagerEvent.StreamDevicesChanged::class) + .map { mediaSessionManager.getActiveSessions(null) }, + ) + .onStart { emit(mediaSessionManager.getActiveSessions(null)) } .flowOn(backgroundContext) - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) - - private fun findLocalMediaController( - controllers: Collection<MediaController>, - ): MediaController? { - var localController: MediaController? = null - val remoteMediaSessionLists: MutableList<String> = ArrayList() - for (controller in controllers) { - val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue - when (playbackInfo.playbackType) { - MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { - if (localController?.packageName.equals(controller.packageName)) { - localController = null - } - if (!remoteMediaSessionLists.contains(controller.packageName)) { - remoteMediaSessionLists.add(controller.packageName) - } - } - MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { - if ( - localController == null && - !remoteMediaSessionLists.contains(controller.packageName) - ) { - localController = controller - } - } - } - } - return localController - } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt index c9ac97dcab7f..778653b9bd44 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt @@ -66,6 +66,10 @@ class AudioVolumeInteractor( } } + fun isMutable(audioStream: AudioStream): Boolean = + // Alarm stream doesn't support muting + audioStream.value != AudioManager.STREAM_ALARM + private suspend fun processVolume( audioStreamModel: AudioStreamModel, ringerMode: RingerMode, diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt deleted file mode 100644 index f6213351ae0d..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt +++ /dev/null @@ -1,57 +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.settingslib.volume.domain.interactor - -import com.android.settingslib.media.MediaDevice -import com.android.settingslib.volume.data.repository.LocalMediaRepository -import com.android.settingslib.volume.domain.model.RoutingSession -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -class LocalMediaInteractor( - private val repository: LocalMediaRepository, - coroutineScope: CoroutineScope, -) { - - /** Available devices list */ - val mediaDevices: StateFlow<Collection<MediaDevice>> - get() = repository.mediaDevices - - /** Currently connected media device */ - val currentConnectedDevice: StateFlow<MediaDevice?> - get() = repository.currentConnectedDevice - - val remoteRoutingSessions: StateFlow<List<RoutingSession>> = - repository.remoteRoutingSessions - .map { sessions -> - sessions.map { - RoutingSession( - routingSessionInfo = it.routingSessionInfo, - isMediaOutputDisabled = it.isMediaOutputDisabled, - isVolumeSeekBarEnabled = - it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0 - ) - } - } - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) - - suspend fun adjustSessionVolume(sessionId: String?, volume: Int) = - repository.adjustSessionVolume(sessionId, volume) -} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt index 2d12dae36ff1..caf41f21afb7 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt @@ -15,17 +15,12 @@ */ package com.android.settingslib.volume.data.repository -import android.media.MediaRoute2Info -import android.media.MediaRouter2Manager -import android.media.RoutingSessionInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice -import com.android.settingslib.volume.data.model.RoutingSession import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope @@ -37,15 +32,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.any -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.anyString -import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @SmallTest class LocalMediaRepositoryImplTest { @@ -53,7 +43,6 @@ class LocalMediaRepositoryImplTest { @Mock private lateinit var localMediaManager: LocalMediaManager @Mock private lateinit var mediaDevice1: MediaDevice @Mock private lateinit var mediaDevice2: MediaDevice - @Mock private lateinit var mediaRouter2Manager: MediaRouter2Manager @Captor private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback> @@ -71,29 +60,11 @@ class LocalMediaRepositoryImplTest { LocalMediaRepositoryImpl( eventsReceiver, localMediaManager, - mediaRouter2Manager, testScope.backgroundScope, - testScope.testScheduler, ) } @Test - fun mediaDevices_areUpdated() { - testScope.runTest { - var mediaDevices: Collection<MediaDevice>? = null - underTest.mediaDevices.onEach { mediaDevices = it }.launchIn(backgroundScope) - runCurrent() - verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture()) - deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2)) - runCurrent() - - assertThat(mediaDevices).hasSize(2) - assertThat(mediaDevices).contains(mediaDevice1) - assertThat(mediaDevices).contains(mediaDevice2) - } - } - - @Test fun deviceListUpdated_currentConnectedDeviceUpdated() { testScope.runTest { var currentConnectedDevice: MediaDevice? = null @@ -110,78 +81,4 @@ class LocalMediaRepositoryImplTest { assertThat(currentConnectedDevice).isEqualTo(mediaDevice1) } } - - @Test - fun kek() { - testScope.runTest { - `when`(localMediaManager.remoteRoutingSessions) - .thenReturn( - listOf( - testRoutingSessionInfo1, - testRoutingSessionInfo2, - testRoutingSessionInfo3, - ) - ) - `when`(localMediaManager.shouldEnableVolumeSeekBar(any())).then { - (it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo1 - } - `when`(mediaRouter2Manager.getTransferableRoutes(any<RoutingSessionInfo>())).then { - if ((it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo2) { - return@then listOf(mock(MediaRoute2Info::class.java)) - } - emptyList<MediaRoute2Info>() - } - var remoteRoutingSessions: Collection<RoutingSession>? = null - underTest.remoteRoutingSessions - .onEach { remoteRoutingSessions = it } - .launchIn(backgroundScope) - - runCurrent() - - assertThat(remoteRoutingSessions) - .containsExactlyElementsIn( - listOf( - RoutingSession( - routingSessionInfo = testRoutingSessionInfo1, - isVolumeSeekBarEnabled = true, - isMediaOutputDisabled = true, - ), - RoutingSession( - routingSessionInfo = testRoutingSessionInfo2, - isVolumeSeekBarEnabled = false, - isMediaOutputDisabled = false, - ), - RoutingSession( - routingSessionInfo = testRoutingSessionInfo3, - isVolumeSeekBarEnabled = false, - isMediaOutputDisabled = true, - ) - ) - ) - } - } - - @Test - fun adjustSessionVolume_adjusts() { - testScope.runTest { - var volume = 0 - `when`(localMediaManager.adjustSessionVolume(anyString(), anyInt())).then { - volume = it.arguments[1] as Int - Unit - } - - underTest.adjustSessionVolume("test_session", 10) - - assertThat(volume).isEqualTo(10) - } - } - - private companion object { - val testRoutingSessionInfo1 = - RoutingSessionInfo.Builder("id_1", "test.pkg.1").addSelectedRoute("route_1").build() - val testRoutingSessionInfo2 = - RoutingSessionInfo.Builder("id_2", "test.pkg.2").addSelectedRoute("route_2").build() - val testRoutingSessionInfo3 = - RoutingSessionInfo.Builder("id_3", "test.pkg.3").addSelectedRoute("route_3").build() - } } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt index f3d17141334e..964c3f7d13d4 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt @@ -22,13 +22,10 @@ import android.media.session.MediaSessionManager import android.media.session.PlaybackState import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.settingslib.bluetooth.BluetoothCallback import com.android.settingslib.bluetooth.BluetoothEventManager import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver -import com.android.settingslib.volume.shared.model.AudioManagerEvent import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope @@ -37,21 +34,15 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.any -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @SmallTest class MediaControllerRepositoryImplTest { - @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback> - @Mock private lateinit var mediaSessionManager: MediaSessionManager @Mock private lateinit var localBluetoothManager: LocalBluetoothManager @Mock private lateinit var eventManager: BluetoothEventManager @@ -103,7 +94,7 @@ class MediaControllerRepositoryImplTest { } @Test - fun playingMediaDevicesAvailable_sessionIsActive() { + fun mediaDevicesAvailable_returnsAllActiveOnes() { testScope.runTest { `when`(mediaSessionManager.getActiveSessions(any())) .thenReturn( @@ -112,53 +103,25 @@ class MediaControllerRepositoryImplTest { statelessMediaController, errorMediaController, remoteMediaController, - localMediaController + localMediaController, ) ) - var mediaController: MediaController? = null - underTest.activeLocalMediaController - .onEach { mediaController = it } - .launchIn(backgroundScope) - runCurrent() - eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged) - triggerOnAudioModeChanged() + var mediaControllers: Collection<MediaController>? = null + underTest.activeSessions.onEach { mediaControllers = it }.launchIn(backgroundScope) runCurrent() - assertThat(mediaController).isSameInstanceAs(localMediaController) - } - } - - @Test - fun noPlayingMediaDevicesAvailable_sessionIsInactive() { - testScope.runTest { - `when`(mediaSessionManager.getActiveSessions(any())) - .thenReturn( - listOf( - stoppedMediaController, - statelessMediaController, - errorMediaController, - ) + assertThat(mediaControllers) + .containsExactly( + stoppedMediaController, + statelessMediaController, + errorMediaController, + remoteMediaController, + localMediaController, ) - var mediaController: MediaController? = null - underTest.activeLocalMediaController - .onEach { mediaController = it } - .launchIn(backgroundScope) - runCurrent() - - eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged) - triggerOnAudioModeChanged() - runCurrent() - - assertThat(mediaController).isNull() } } - private fun triggerOnAudioModeChanged() { - verify(eventManager).registerCallback(callbackCaptor.capture()) - callbackCaptor.value.onAudioModeChanged() - } - private companion object { val statePlaying: PlaybackState = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build() diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java index 6f31fad104d0..0931b685d967 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java @@ -16,7 +16,6 @@ package com.android.settingslib; import static com.android.settingslib.Utils.STORAGE_MANAGER_ENABLED_PROPERTY; -import static com.android.settingslib.Utils.shouldShowWirelessChargingWarningTip; import static com.google.common.truth.Truth.assertThat; @@ -543,20 +542,6 @@ public class UtilsTest { assertThat(Utils.containsIncompatibleChargers(mContext, TAG)).isFalse(); } - @Test - public void shouldShowWirelessChargingWarningTip_enabled_returnTrue() { - Utils.updateWirelessChargingWarningEnabled(mContext, true, TAG); - - assertThat(shouldShowWirelessChargingWarningTip(mContext, TAG)).isTrue(); - } - - @Test - public void shouldShowWirelessChargingWarningTip_disabled_returnFalse() { - Utils.updateWirelessChargingWarningEnabled(mContext, false, TAG); - - assertThat(shouldShowWirelessChargingWarningTip(mContext, TAG)).isFalse(); - } - private void setupIncompatibleCharging() { setupIncompatibleCharging(UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java index f9505ddb7e2f..52622a7f1875 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/applications/RecentAppOpsAccessesTest.java @@ -32,14 +32,17 @@ import android.content.PermissionChecker; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.UserProperties; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.LongSparseArray; import com.android.settingslib.testutils.shadow.ShadowPermissionChecker; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -58,6 +61,8 @@ import java.util.concurrent.TimeUnit; @Config(shadows = {ShadowPermissionChecker.class}) public class RecentAppOpsAccessesTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final int TEST_UID = 1234; private static final long NOW = 1_000_000_000; // Approximately 9/8/2001 private static final long ONE_MIN_AGO = NOW - TimeUnit.MINUTES.toMillis(1); @@ -73,6 +78,8 @@ public class RecentAppOpsAccessesTest { @Mock private UserManager mUserManager; @Mock + private UserProperties mUserProperties; + @Mock private Clock mClock; private Context mContext; private int mTestUserId; @@ -132,6 +139,58 @@ public class RecentAppOpsAccessesTest { } @Test + public void testGetAppList_quietModeDisabled_shouldFilterRecentAccesses() { + mSetFlagsRule.enableFlags( + android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE, + android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE); + when(mUserManager.isQuietModeEnabled(any())).thenReturn(false); + + List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(false); + // Only two of the apps have requested location within 15 min. + assertThat(requests).hasSize(2); + // Make sure apps are ordered by recency + assertThat(requests.get(0).packageName).isEqualTo(TEST_PACKAGE_NAMES[0]); + assertThat(requests.get(0).accessFinishTime).isEqualTo(ONE_MIN_AGO); + assertThat(requests.get(1).packageName).isEqualTo(TEST_PACKAGE_NAMES[1]); + assertThat(requests.get(1).accessFinishTime).isEqualTo(TWENTY_THREE_HOURS_AGO); + } + + @Test + public void testGetAppList_quietModeEnabledShowInQuietDefault_shouldFilterRecentAccesses() { + mSetFlagsRule.enableFlags( + android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE, + android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE); + when(mUserManager.isQuietModeEnabled(any())).thenReturn(true); + when(mUserManager.getUserProperties(any())).thenReturn(mUserProperties); + when(mUserProperties.getShowInQuietMode()) + .thenReturn(UserProperties.SHOW_IN_QUIET_MODE_DEFAULT); + + List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(false); + // Only two of the apps have requested location within 15 min. + assertThat(requests).hasSize(2); + // Make sure apps are ordered by recency + assertThat(requests.get(0).packageName).isEqualTo(TEST_PACKAGE_NAMES[0]); + assertThat(requests.get(0).accessFinishTime).isEqualTo(ONE_MIN_AGO); + assertThat(requests.get(1).packageName).isEqualTo(TEST_PACKAGE_NAMES[1]); + assertThat(requests.get(1).accessFinishTime).isEqualTo(TWENTY_THREE_HOURS_AGO); + } + + @Test + public void testGetAppList_quietModeEnabledShowInQuietHidden_shouldNotFilterRecentAccesses() { + mSetFlagsRule.enableFlags( + android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE, + android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE); + when(mUserManager.isQuietModeEnabled(any())).thenReturn(true); + when(mUserManager.getUserProperties(any())).thenReturn(mUserProperties); + when(mUserProperties.getShowInQuietMode()) + .thenReturn(UserProperties.SHOW_IN_QUIET_MODE_HIDDEN); + + List<RecentAppOpsAccess.Access> requests = mRecentAppOpsAccess.getAppList(false); + // Apps doesn't show up in the list of apps. + assertThat(requests).hasSize(0); + } + + @Test public void testGetAppList_shouldNotShowAndroidOS() throws NameNotFoundException { // Add android OS to the list of apps. PackageOps androidSystemPackageOps = diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java index 13635c3a8256..48bbf4ea6a65 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java @@ -18,6 +18,7 @@ package com.android.settingslib.bluetooth; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -489,4 +490,17 @@ public class BluetoothEventManagerTest { verify(mErrorListener).onShowError(any(Context.class), eq(DEVICE_NAME), eq(R.string.bluetooth_pairing_pin_error_message)); } + + /** + * Intent ACTION_AUTO_ON_STATE_CHANGED should dispatch to callback. + */ + @Test + public void intentWithExtraState_autoOnStateChangedShouldDispatchToRegisterCallback() { + mBluetoothEventManager.registerCallback(mBluetoothCallback); + mIntent = new Intent(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED); + + mContext.sendBroadcast(mIntent); + + verify(mBluetoothCallback).onAutoOnStateChanged(anyInt()); + } } diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp index 7ec3d243529f..bf4f60d84e4d 100644 --- a/packages/SettingsProvider/Android.bp +++ b/packages/SettingsProvider/Android.bp @@ -60,6 +60,7 @@ android_test { // because this test is not an instrumentation test. (because the target runs in the system process.) "SettingsProviderLib", "androidx.test.rules", + "frameworks-base-testutils", "device_config_service_flags_java", "flag-junit", "junit", diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index eaec617cfa70..87a7f823edfe 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -256,8 +256,7 @@ public class SecureSettings { Settings.Secure.HEARING_AID_MEDIA_ROUTING, Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING, Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, - Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, - Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED, + Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, Settings.Secure.HUB_MODE_TUTORIAL_STATE, Settings.Secure.STYLUS_BUTTONS_ENABLED, Settings.Secure.STYLUS_HANDWRITING_ENABLED, @@ -270,6 +269,7 @@ public class SecureSettings { Settings.Secure.CAMERA_EXTENSIONS_FALLBACK, Settings.Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, - Settings.Secure.AUDIO_DEVICE_INVENTORY + Settings.Secure.AUDIO_DEVICE_INVENTORY, + Settings.Secure.ACCESSIBILITY_FLOATING_MENU_TARGETS }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 046d6e25ff31..edef286b6ac0 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -208,8 +208,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.ASSIST_TOUCH_GESTURE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ASSIST_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, BOOLEAN_VALIDATOR); - VALIDATORS.put(Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, BOOLEAN_VALIDATOR); - VALIDATORS.put(Secure.SEARCH_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VR_DISPLAY_MODE, new DiscreteValueValidator(new String[] {"0", "1"})); VALIDATORS.put(Secure.NOTIFICATION_BADGING, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.NOTIFICATION_DISMISS_RTL, BOOLEAN_VALIDATOR); @@ -327,6 +326,9 @@ public class SecureSettingsValidators { Secure.ACCESSIBILITY_BUTTON_TARGETS, ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR); VALIDATORS.put( + Secure.ACCESSIBILITY_FLOATING_MENU_TARGETS, + ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR); + VALIDATORS.put( Secure.ACCESSIBILITY_QS_TARGETS, ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java index e5d62f8f9fac..8e320054028c 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -75,7 +75,10 @@ import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.zip.CRC32; /** @@ -103,6 +106,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { // fatal crash. Creating a backup with a different key will prevent Android 12 versions from // restoring this data. private static final String KEY_SIM_SPECIFIC_SETTINGS_2 = "sim_specific_settings_2"; + private static final String KEY_WIFI_SETTINGS_BACKUP_DATA = "wifi_settings_backup_data"; // Versioning of the state file. Increment this version // number any time the set of state items is altered. @@ -126,8 +130,9 @@ public class SettingsBackupAgent extends BackupAgentHelper { private static final int STATE_WIFI_NEW_CONFIG = 9; private static final int STATE_DEVICE_CONFIG = 10; private static final int STATE_SIM_SPECIFIC_SETTINGS = 11; + private static final int STATE_WIFI_SETTINGS = 12; - private static final int STATE_SIZE = 12; // The current number of state items + private static final int STATE_SIZE = 13; // The current number of state items // Number of entries in the checksum array at various version numbers private static final int STATE_SIZES[] = { @@ -140,7 +145,8 @@ public class SettingsBackupAgent extends BackupAgentHelper { 9, // version 6 added STATE_NETWORK_POLICIES 10, // version 7 added STATE_WIFI_NEW_CONFIG 11, // version 8 added STATE_DEVICE_CONFIG - STATE_SIZE // version 9 added STATE_SIM_SPECIFIC_SETTINGS + 12, // version 9 added STATE_SIM_SPECIFIC_SETTINGS + STATE_SIZE // version 10 added STATE_WIFI_SETTINGS }; private static final int FULL_BACKUP_ADDED_GLOBAL = 2; // added the "global" entry @@ -230,6 +236,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { byte[] wifiFullConfigData = getNewWifiConfigData(); byte[] deviceSpecificInformation = getDeviceSpecificConfiguration(); byte[] simSpecificSettingsData = getSimSpecificSettingsData(); + byte[] wifiSettingsData = getWifiSettingsBackupData(); long[] stateChecksums = readOldChecksums(oldState); @@ -265,6 +272,9 @@ public class SettingsBackupAgent extends BackupAgentHelper { stateChecksums[STATE_SIM_SPECIFIC_SETTINGS] = writeIfChanged(stateChecksums[STATE_SIM_SPECIFIC_SETTINGS], KEY_SIM_SPECIFIC_SETTINGS_2, simSpecificSettingsData, data); + stateChecksums[STATE_WIFI_SETTINGS] = + writeIfChanged(stateChecksums[STATE_WIFI_SETTINGS], + KEY_WIFI_SETTINGS_BACKUP_DATA, wifiSettingsData, data); writeNewChecksums(stateChecksums, newState); } @@ -413,7 +423,13 @@ public class SettingsBackupAgent extends BackupAgentHelper { data.readEntityData(restoredSimSpecificSettings, 0, size); restoreSimSpecificSettings(restoredSimSpecificSettings); break; - + case KEY_WIFI_SETTINGS_BACKUP_DATA: + byte[] restoredWifiData = new byte[size]; + data.readEntityData(restoredWifiData, 0, size); + if (!isWatch()) { + restoreWifiData(restoredWifiData); + } + break; default : data.skipEntityData(); @@ -1346,6 +1362,45 @@ public class SettingsBackupAgent extends BackupAgentHelper { } } + private static final class Mutable<E> { + public volatile E value; + + Mutable() { + value = null; + } + } + + private byte[] getWifiSettingsBackupData() { + final CountDownLatch latch = new CountDownLatch(1); + final Mutable<byte[]> backupWifiData = new Mutable<byte[]>(); + + try { + mWifiManager.retrieveWifiBackupData(getBaseContext().getMainExecutor(), + new Consumer<byte[]>() { + @Override + public void accept(byte[] value) { + backupWifiData.value = value; + latch.countDown(); + } + }); + // cts requires B&R with 10 seconds + if (latch.await(10, TimeUnit.SECONDS) && backupWifiData.value != null) { + return backupWifiData.value; + } + } catch (InterruptedException ie) { + Log.e(TAG, "fail to retrieveWifiBackupData, " + ie); + } + Log.e(TAG, "fail to retrieveWifiBackupData"); + return new byte[0]; + } + + private void restoreWifiData(byte[] data) { + if (DEBUG_BACKUP) { + Log.v(TAG, "Applying restored all wifi data"); + } + mWifiManager.restoreWifiBackupData(data); + } + private void updateWindowManagerIfNeeded(Integer previousDensity) { int newDensity; try { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index 3e0d05cd9ecf..1eb04ac1c181 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -98,6 +98,7 @@ public class SettingsHelper { sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_END_TIME); sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED); sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); + sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_QS_TARGETS); sBroadcastOnRestoreSystemUI = new ArraySet<String>(2); sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_TILES); sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_AUTO_ADDED_TILES); @@ -229,6 +230,10 @@ public class SettingsHelper { } else if (Settings.System.ACCELEROMETER_ROTATION.equals(name) && shouldSkipAutoRotateRestore()) { return; + } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(name)) { + // Don't write it to setting. Let the broadcast receiver in + // AccessibilityManagerService handle restore/merging logic. + return; } // Default case: write the restored value to settings diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 02d212cb4996..4603b43b0ab5 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1838,6 +1838,9 @@ class SettingsProtoDumpUtil { Settings.Secure.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED, SecureSettingsProto.Accessibility.ACCESSIBILITY_FLOATING_MENU_FADE_ENABLED); dumpSetting(s, p, + Settings.Secure.ACCESSIBILITY_FLOATING_MENU_TARGETS, + SecureSettingsProto.Accessibility.ACCESSIBILITY_FLOATING_MENU_TARGETS); + dumpSetting(s, p, Settings.Secure.ODI_CAPTIONS_VOLUME_UI_ENABLED, SecureSettingsProto.Accessibility.ODI_CAPTIONS_VOLUME_UI_ENABLED); dumpSetting(s, p, @@ -1950,11 +1953,8 @@ class SettingsProtoDumpUtil { Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED, SecureSettingsProto.Assist.LONG_PRESS_HOME_ENABLED); dumpSetting(s, p, - Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, - SecureSettingsProto.Assist.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED); - dumpSetting(s, p, - Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED, - SecureSettingsProto.Assist.SEARCH_LONG_PRESS_HOME_ENABLED); + Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, + SecureSettingsProto.Assist.SEARCH_ALL_ENTRYPOINTS_ENABLED); dumpSetting(s, p, Settings.Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, SecureSettingsProto.Assist.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED); diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 6eb2dd043c94..8cafe5faaa09 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -688,6 +688,7 @@ public class SettingsBackupTest { Settings.Secure.DEVICE_PAIRED, Settings.Secure.DIALER_DEFAULT_APPLICATION, Settings.Secure.DISABLED_PRINT_SERVICES, + Settings.Secure.DISABLE_SECURE_WINDOWS, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, Settings.Secure.DOCKED_CLOCK_FACE, Settings.Secure.DOZE_PULSE_ON_LONG_PRESS, diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java index 197788e11973..2f8cf4b3d034 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java @@ -16,23 +16,31 @@ package com.android.providers.settings; +import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertEquals; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; +import android.content.Intent; import android.net.Uri; import android.os.Build; import android.provider.Settings; +import android.provider.SettingsStringUtil; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.util.test.BroadcastInterceptingContext; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import java.util.concurrent.ExecutionException; + /** * Tests for {@link SettingsHelper#restoreValue(Context, ContentResolver, ContentValues, Uri, * String, String, int)}. Specifically verifies that we restore critical accessibility settings only @@ -165,4 +173,33 @@ public class SettingsHelperRestoreTest { assertEquals(restoreSettingValue, Settings.Secure.getInt(mContentResolver, settingName)); } + + @Test + public void restoreAccessibilityQsTargets_broadcastSent() + throws ExecutionException, InterruptedException { + BroadcastInterceptingContext interceptingContext = new BroadcastInterceptingContext( + mContext); + final String settingName = Settings.Secure.ACCESSIBILITY_QS_TARGETS; + final String restoreSettingValue = "com.android.server.accessibility/ColorInversion" + + SettingsStringUtil.DELIMITER + + "com.android.server.accessibility/ColorCorrectionTile"; + BroadcastInterceptingContext.FutureIntent futureIntent = + interceptingContext.nextBroadcastIntent(Intent.ACTION_SETTING_RESTORED); + + mSettingsHelper.restoreValue( + interceptingContext, + mContentResolver, + new ContentValues(2), + Settings.Secure.getUriFor(settingName), + settingName, + restoreSettingValue, + Build.VERSION.SDK_INT); + + Intent intentReceived = futureIntent.get(); + assertThat(intentReceived.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE)) + .isEqualTo(restoreSettingValue); + assertThat(intentReceived.getIntExtra( + Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, /* defaultValue= */ 0)) + .isEqualTo(Build.VERSION.SDK_INT); + } } diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 02d19dc84f2e..58040716db3e 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -932,6 +932,9 @@ <uses-permission android:name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" /> + <!-- Permission required for Cts test - CtsSettingsTestCases --> + <uses-permission android:name="android.permission.PREPARE_FACTORY_RESET" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java index 6546b87c8802..f70ad9ed58b0 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java @@ -23,10 +23,10 @@ import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_QU import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS; import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT; -import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION_EXTRA; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU; +import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME; @@ -77,6 +77,8 @@ public class AccessibilityMenuServiceTest { private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5; private static final int TIMEOUT_UI_CHANGE_S = 5; private static final int NO_GLOBAL_ACTION = -1; + private static final Intent INTENT_OPEN_MENU = new Intent(INTENT_TOGGLE_MENU) + .setPackage(PACKAGE_NAME); private static Instrumentation sInstrumentation; private static UiAutomation sUiAutomation; @@ -152,9 +154,6 @@ public class AccessibilityMenuServiceTest { @Before public void setup() throws Throwable { sOpenBlocked.set(false); - wakeUpScreen(); - sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU"); - openMenu(); } @After @@ -188,24 +187,17 @@ public class AccessibilityMenuServiceTest { } private static void openMenu() throws Throwable { - openMenu(false); - } - - private static void openMenu(boolean abandonOnBlock) throws Throwable { - Intent intent = new Intent(INTENT_TOGGLE_MENU); - intent.setPackage(PACKAGE_NAME); - sInstrumentation.getContext().sendBroadcast(intent); + unlockSignal(); + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); TestUtils.waitUntil("Timed out before menu could appear.", TIMEOUT_UI_CHANGE_S, () -> { - if (sOpenBlocked.get() && abandonOnBlock) { - throw new IllegalStateException(); - } if (isMenuVisible()) { return true; } else { - sInstrumentation.getContext().sendBroadcast(intent); + unlockSignal(); + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); return false; } }); @@ -249,6 +241,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAdjustBrightness() throws Throwable { + openMenu(); Context context = sInstrumentation.getTargetContext(); DisplayManager displayManager = context.getSystemService( DisplayManager.class); @@ -264,22 +257,28 @@ public class AccessibilityMenuServiceTest { context.getDisplayId()).getBrightnessInfo(); try { - displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum); TestUtils.waitUntil("Could not change to minimum brightness", TIMEOUT_UI_CHANGE_S, - () -> displayManager.getBrightness(context.getDisplayId()) - == brightnessInfo.brightnessMinimum); + () -> { + displayManager.setBrightness( + context.getDisplayId(), brightnessInfo.brightnessMinimum); + return displayManager.getBrightness(context.getDisplayId()) + == brightnessInfo.brightnessMinimum; + }); brightnessUpButton.performAction(CLICK_ID); TestUtils.waitUntil("Did not detect an increase in brightness.", TIMEOUT_UI_CHANGE_S, () -> displayManager.getBrightness(context.getDisplayId()) > brightnessInfo.brightnessMinimum); - displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum); TestUtils.waitUntil("Could not change to maximum brightness", TIMEOUT_UI_CHANGE_S, - () -> displayManager.getBrightness(context.getDisplayId()) - == brightnessInfo.brightnessMaximum); + () -> { + displayManager.setBrightness( + context.getDisplayId(), brightnessInfo.brightnessMaximum); + return displayManager.getBrightness(context.getDisplayId()) + == brightnessInfo.brightnessMaximum; + }); brightnessDownButton.performAction(CLICK_ID); TestUtils.waitUntil("Did not detect a decrease in brightness.", TIMEOUT_UI_CHANGE_S, @@ -292,6 +291,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAdjustVolume() throws Throwable { + openMenu(); Context context = sInstrumentation.getTargetContext(); AudioManager audioManager = context.getSystemService(AudioManager.class); int resetVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); @@ -332,6 +332,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAssistantButton_opensVoiceAssistant() throws Throwable { + openMenu(); AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal())); Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND); @@ -349,6 +350,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAccessibilitySettingsButton_opensAccessibilitySettings() throws Throwable { + openMenu(); AccessibilityNodeInfo settingsButton = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_A11YSETTING_VALUE.ordinal())); Intent expectedIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); @@ -364,6 +366,7 @@ public class AccessibilityMenuServiceTest { @Test public void testPowerButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_POWER_VALUE.ordinal())); @@ -376,6 +379,7 @@ public class AccessibilityMenuServiceTest { @Test public void testRecentButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_RECENT_VALUE.ordinal())); @@ -388,6 +392,7 @@ public class AccessibilityMenuServiceTest { @Test public void testLockButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_LOCKSCREEN_VALUE.ordinal())); @@ -400,6 +405,7 @@ public class AccessibilityMenuServiceTest { @Test public void testQuickSettingsButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_QUICKSETTING_VALUE.ordinal())); @@ -412,6 +418,7 @@ public class AccessibilityMenuServiceTest { @Test public void testNotificationsButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_NOTIFICATION_VALUE.ordinal())); @@ -424,6 +431,7 @@ public class AccessibilityMenuServiceTest { @Test public void testScreenshotButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_SCREENSHOT_VALUE.ordinal())); @@ -436,6 +444,7 @@ public class AccessibilityMenuServiceTest { @Test public void testOnScreenLock_closesMenu() throws Throwable { + openMenu(); closeScreen(); wakeUpScreen(); @@ -447,13 +456,18 @@ public class AccessibilityMenuServiceTest { closeScreen(); wakeUpScreen(); - boolean blocked = false; - try { - openMenu(true); - } catch (IllegalStateException e) { - // Expected - blocked = true; - } - assertThat(blocked).isTrue(); + TestUtils.waitUntil("Did not receive signal that menu cannot open", + TIMEOUT_UI_CHANGE_S, + () -> { + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); + return sOpenBlocked.get(); + }); + } + + private static void unlockSignal() { + // MENU unlocks screen, + // BACK closes any menu that may appear if the screen wasn't locked. + sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU"); + sUiAutomation.executeShellCommand("input keyevent KEYCODE_BACK"); } } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 8da50216f13c..f057acc71b98 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -25,6 +25,16 @@ flag { } flag { + name: "notification_minimalism_prototype" + namespace: "systemui" + description: "Prototype of notification minimalism; the new 'Intermediate' lockscreen customization proposal." + bug: "330387368" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "notification_view_flipper_pausing" namespace: "systemui" description: "Pause ViewFlippers inside Notification custom layouts when the shade is closed." @@ -104,6 +114,13 @@ flag { } flag { + name: "notifications_heads_up_refactor" + namespace: "systemui" + description: "Use HeadsUpInteractor to feed HUN updates to the NSSL." + bug: "325936094" +} + +flag { name: "pss_app_selector_abrupt_exit_fix" namespace: "systemui" description: "Fixes the app selector abruptly disappearing without an animation, when the" @@ -424,6 +441,13 @@ flag { } flag { + name: "screenshot_shelf_ui" + namespace: "systemui" + description: "Use new shelf UI flow for screenshots" + bug: "329659738" +} + +flag { name: "run_fingerprint_detect_on_dismissible_keyguard" namespace: "systemui" description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible." diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt index abe1e3de8eea..1c763e8c6108 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt @@ -181,6 +181,11 @@ private constructor( turbulenceNoiseShader.setColor(newColor) } + /** Updates the noise color that's screen blended on top. */ + fun updateScreenColor(newColor: Int) { + turbulenceNoiseShader.setScreenColor(newColor) + } + /** * Retrieves the noise offset x, y, z values. This is useful for replaying the animation * smoothly from the last animation, by passing in the last values to the next animation. @@ -322,7 +327,10 @@ private constructor( private fun draw() { paintCallback?.onDraw(paint!!) renderEffectCallback?.onDraw( - RenderEffect.createRuntimeShaderEffect(turbulenceNoiseShader, "in_src") + RenderEffect.createRuntimeShaderEffect( + turbulenceNoiseShader, + TurbulenceNoiseShader.BACKGROUND_UNIFORM + ) ) } diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt index 59354c843447..ba8f1ace0214 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseAnimationConfig.kt @@ -52,7 +52,7 @@ data class TurbulenceNoiseAnimationConfig( /** Color of the effect. */ val color: Int = DEFAULT_COLOR, /** Background color of the effect. */ - val backgroundColor: Int = DEFAULT_BACKGROUND_COLOR, + val screenColor: Int = DEFAULT_SCREEN_COLOR, val width: Float = 0f, val height: Float = 0f, val maxDuration: Float = DEFAULT_MAX_DURATION_IN_MILLIS, @@ -72,7 +72,7 @@ data class TurbulenceNoiseAnimationConfig( */ val lumaMatteOverallBrightness: Float = DEFAULT_LUMA_MATTE_OVERALL_BRIGHTNESS, /** Whether to flip the luma mask. */ - val shouldInverseNoiseLuminosity: Boolean = false + val shouldInverseNoiseLuminosity: Boolean = false, ) { companion object { const val DEFAULT_MAX_DURATION_IN_MILLIS = 30_000f // Max 30 sec @@ -83,7 +83,7 @@ data class TurbulenceNoiseAnimationConfig( const val DEFAULT_COLOR = Color.WHITE const val DEFAULT_LUMA_MATTE_BLEND_FACTOR = 1f const val DEFAULT_LUMA_MATTE_OVERALL_BRIGHTNESS = 0f - const val DEFAULT_BACKGROUND_COLOR = Color.BLACK + const val DEFAULT_SCREEN_COLOR = Color.BLACK private val random = Random() } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt index 8dd90a8ffe9f..025c8b9dce04 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt @@ -16,6 +16,7 @@ package com.android.systemui.surfaceeffects.turbulencenoise import android.graphics.RuntimeShader +import com.android.systemui.surfaceeffects.shaders.SolidColorShader import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary import java.lang.Float.max @@ -28,9 +29,11 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : RuntimeShader(getShader(baseType)) { // language=AGSL companion object { + /** Uniform name for the background buffer (e.g. image, solid color, etc.). */ + const val BACKGROUND_UNIFORM = "in_src" private const val UNIFORMS = """ - uniform shader in_src; // Needed to support RenderEffect. + uniform shader ${BACKGROUND_UNIFORM}; uniform float in_gridNum; uniform vec3 in_noiseMove; uniform vec2 in_size; @@ -41,7 +44,7 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : uniform half in_lumaMatteBlendFactor; uniform half in_lumaMatteOverallBrightness; layout(color) uniform vec4 in_color; - layout(color) uniform vec4 in_backgroundColor; + layout(color) uniform vec4 in_screenColor; """ private const val SIMPLEX_SHADER = @@ -50,22 +53,20 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : vec2 uv = p / in_size.xy; uv.x *= in_aspectRatio; + // Compute turbulence effect with the uv distorted with simplex noise. vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; - // Bring it to [0, 1] range. - float luma = (simplex3d(noiseP) * in_inverseLuma) * 0.5 + 0.5; - luma = saturate(luma * in_lumaMatteBlendFactor + in_lumaMatteOverallBrightness) - * in_opacity; - vec3 mask = maskLuminosity(in_color.rgb, luma); - vec3 color = in_backgroundColor.rgb + mask * 0.6; + vec3 color = getColorTurbulenceMask(simplex3d(noiseP) * in_inverseLuma); + + // Blend the result with the background color. + color = in_src.eval(p).rgb + color * 0.6; // Add dither with triangle distribution to avoid color banding. Dither in the // shader here as we are in gamma space. float dither = triangleNoise(p * in_pixelDensity) / 255.; + color += dither.rrr; - // The result color should be pre-multiplied, i.e. [R*A, G*A, B*A, A], thus need to - // multiply rgb with a to get the correct result. - color = (color + dither.rrr) * in_opacity; - return vec4(color, in_opacity); + // Return the pre-multiplied alpha result, i.e. [R*A, G*A, B*A, A]. + return vec4(color * in_opacity, in_opacity); } """ @@ -76,32 +77,105 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : uv.x *= in_aspectRatio; vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; - // Bring it to [0, 1] range. - float luma = (simplex3d_fractal(noiseP) * in_inverseLuma) * 0.5 + 0.5; - luma = saturate(luma * in_lumaMatteBlendFactor + in_lumaMatteOverallBrightness) - * in_opacity; - vec3 mask = maskLuminosity(in_color.rgb, luma); - vec3 color = in_backgroundColor.rgb + mask * 0.6; + vec3 color = getColorTurbulenceMask(simplex3d_fractal(noiseP) * in_inverseLuma); + + // Blend the result with the background color. + color = in_src.eval(p).rgb + color * 0.6; // Skip dithering. return vec4(color * in_opacity, in_opacity); } """ + + /** + * This effect has two layers: color turbulence effect with sparkles on top. + * 1. Gets the luma matte using Simplex noise. + * 2. Generate a colored turbulence layer with the luma matte. + * 3. Generate a colored sparkle layer with the same luma matter. + * 4. Apply a screen color to the background image. + * 5. Composite the previous result with the color turbulence. + * 6. Composite the latest result with the sparkles. + */ + private const val SIMPLEX_SPARKLE_SHADER = + """ + vec4 main(vec2 p) { + vec2 uv = p / in_size.xy; + uv.x *= in_aspectRatio; + + vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; + // Luma is used for both color and sparkle masks. + float luma = simplex3d(noiseP) * in_inverseLuma; + + // Get color layer (color mask with in_color applied) + vec3 colorLayer = getColorTurbulenceMask(simplex3d(noiseP) * in_inverseLuma); + float dither = triangleNoise(p * in_pixelDensity) / 255.; + colorLayer += dither.rrr; + + // Get sparkle layer (sparkle mask with particles & in_color applied) + vec3 sparkleLayer = getSparkleTurbulenceMask(luma, p); + + // Composite with the background. + half4 bgColor = in_src.eval(p); + half sparkleOpacity = smoothstep(0, 0.75, in_opacity); + + half3 effect = screen(bgColor.rgb, in_screenColor.rgb); + effect = screen(effect, colorLayer * 0.22); + effect += sparkleLayer * sparkleOpacity; + + return mix(bgColor, vec4(effect, 1.), in_opacity); + } + """ + + private const val COMMON_FUNCTIONS = + /** + * Below two functions generate turbulence layers (color or sparkles applied) with the + * given luma matte. They both return a mask with in_color applied. + */ + """ + vec3 getColorTurbulenceMask(float luma) { + // Bring it to [0, 1] range. + luma = luma * 0.5 + 0.5; + + half colorLuma = + saturate(luma * in_lumaMatteBlendFactor + in_lumaMatteOverallBrightness) + * in_opacity; + vec3 colorLayer = maskLuminosity(in_color.rgb, colorLuma); + + return colorLayer; + } + + vec3 getSparkleTurbulenceMask(float luma, vec2 p) { + half lumaIntensity = 1.75; + half lumaBrightness = -1.3; + half sparkleLuma = max(luma * lumaIntensity + lumaBrightness, 0.); + + float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_noiseMove.z); + vec3 sparkleLayer = maskLuminosity(in_color.rgb * sparkle, sparkleLuma); + + return sparkleLayer; + } + """ private const val SIMPLEX_NOISE_SHADER = - ShaderUtilLibrary.SHADER_LIB + UNIFORMS + SIMPLEX_SHADER + ShaderUtilLibrary.SHADER_LIB + UNIFORMS + COMMON_FUNCTIONS + SIMPLEX_SHADER private const val FRACTAL_NOISE_SHADER = - ShaderUtilLibrary.SHADER_LIB + UNIFORMS + FRACTAL_SHADER - // TODO (b/282007590): Add NOISE_WITH_SPARKLE + ShaderUtilLibrary.SHADER_LIB + UNIFORMS + COMMON_FUNCTIONS + FRACTAL_SHADER + private const val SPARKLE_NOISE_SHADER = + ShaderUtilLibrary.SHADER_LIB + UNIFORMS + COMMON_FUNCTIONS + SIMPLEX_SPARKLE_SHADER enum class Type { + /** Effect with a simple color noise turbulence. */ SIMPLEX_NOISE, + /** Effect with a simple color noise turbulence, with fractal. */ SIMPLEX_NOISE_FRACTAL, + /** Effect with color & sparkle turbulence with screen color layer. */ + SIMPLEX_NOISE_SPARKLE } fun getShader(type: Type): String { return when (type) { Type.SIMPLEX_NOISE -> SIMPLEX_NOISE_SHADER Type.SIMPLEX_NOISE_FRACTAL -> FRACTAL_NOISE_SHADER + Type.SIMPLEX_NOISE_SPARKLE -> SPARKLE_NOISE_SHADER } } } @@ -111,7 +185,7 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : setGridCount(config.gridCount) setPixelDensity(config.pixelDensity) setColor(config.color) - setBackgroundColor(config.backgroundColor) + setScreenColor(config.screenColor) setSize(config.width, config.height) setLumaMatteFactors(config.lumaMatteBlendFactor, config.lumaMatteOverallBrightness) setInverseNoiseLuminosity(config.shouldInverseNoiseLuminosity) @@ -137,9 +211,20 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : setColorUniform("in_color", color) } - /** Sets the background color of the effect. Alpha is ignored. */ + /** + * Sets the color that is used for blending on top of the background color/image. Only relevant + * to [Type.SIMPLEX_NOISE_SPARKLE]. + */ + fun setScreenColor(color: Int) { + setColorUniform("in_screenColor", color) + } + + /** + * Sets the background color of the effect. Alpha is ignored. If you are using [RenderEffect], + * no need to call this function since the background image of the View will be used. + */ fun setBackgroundColor(color: Int) { - setColorUniform("in_backgroundColor", color) + setInputShader(BACKGROUND_UNIFORM, SolidColorShader(color)) } /** @@ -163,7 +248,7 @@ class TurbulenceNoiseShader(val baseType: Type = Type.SIMPLEX_NOISE) : * * @param lumaMatteBlendFactor increases or decreases the amount of variance in noise. Setting * this a lower number removes variations. I.e. the turbulence noise will look more blended. - * Expected input range is [0, 1]. more dimmed. + * Expected input range is [0, 1]. * @param lumaMatteOverallBrightness adds the overall brightness of the turbulence noise. * Expected input range is [0, 1]. * diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt index 596a297a6dbe..4a89e31bcea8 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt @@ -18,6 +18,7 @@ package com.android.compose +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas @@ -266,8 +267,17 @@ private fun TrackBackground( label = "PlatformSliderCornersAnimation", ) - val trackColor = colors.getTrackColor(enabled) - val indicatorColor = colors.getIndicatorColor(enabled) + val trackColor by + animateColorAsState( + colors.getTrackColor(enabled), + label = "PlatformSliderTrackColorAnimation", + ) + + val indicatorColor by + animateColorAsState( + colors.getIndicatorColor(enabled), + label = "PlatformSliderIndicatorColorAnimation", + ) Canvas(modifier.fillMaxSize()) { val trackCornerRadius = CornerRadius(size.height / 2, size.height / 2) val trackPath = Path() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt index 621ddf796f58..0f3d3dc2847f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt @@ -53,6 +53,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -71,6 +72,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.times import com.android.compose.PlatformButton import com.android.compose.animation.scene.ElementKey @@ -84,7 +86,9 @@ import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.MessageViewModel import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel @@ -166,7 +170,7 @@ private fun StandardLayout( modifier = Modifier.fillMaxWidth(), ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, modifier = Modifier, ) @@ -228,7 +232,7 @@ private fun SplitLayout( when (authMethod) { is PinBouncerViewModel -> { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, modifier = Modifier.align(Alignment.TopCenter), ) @@ -241,7 +245,7 @@ private fun SplitLayout( } is PatternBouncerViewModel -> { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, modifier = Modifier.align(Alignment.TopCenter), ) @@ -280,7 +284,7 @@ private fun SplitLayout( modifier = Modifier.fillMaxWidth().align(Alignment.Center), ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, ) OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp)) @@ -376,7 +380,7 @@ private fun BesideUserSwitcherLayout( modifier = Modifier.fillMaxWidth() ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, ) OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp)) @@ -441,7 +445,7 @@ private fun BelowUserSwitcherLayout( modifier = Modifier.fillMaxWidth(), ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, ) OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp)) @@ -480,6 +484,7 @@ private fun FoldAware( onChangeScene = {}, transitions = SceneTransitions, modifier = modifier, + enableInterruptions = false, ) { scene(SceneKeys.ContiguousSceneKey) { FoldableScene( @@ -548,26 +553,44 @@ private fun SceneScope.FoldableScene( @Composable private fun StatusMessage( - viewModel: BouncerViewModel, + viewModel: BouncerMessageViewModel, modifier: Modifier = Modifier, ) { - val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState() + val message: MessageViewModel? by viewModel.message.collectAsState() + + DisposableEffect(Unit) { + viewModel.onShown() + onDispose {} + } Crossfade( targetState = message, label = "Bouncer message", - animationSpec = if (message.isUpdateAnimated) tween() else snap(), + animationSpec = if (message?.isUpdateAnimated == true) tween() else snap(), modifier = modifier.fillMaxWidth(), - ) { - Box( - contentAlignment = Alignment.Center, + ) { msg -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth(), ) { - Text( - text = it.text, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodyLarge, - ) + msg?.let { + Text( + text = it.text, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 18.sp, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier.size(10.dp)) + Text( + text = it.secondaryText ?: "", + color = MaterialTheme.colorScheme.onSurface, + fontSize = 14.sp, + lineHeight = 20.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt index 2a13d4931b69..c34f2fd26d0c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt @@ -74,10 +74,7 @@ internal fun PasswordBouncer( val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState() val selectedUserId by viewModel.selectedUserId.collectAsState() - DisposableEffect(Unit) { - viewModel.onShown() - onDispose { viewModel.onHidden() } - } + DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } LaunchedEffect(animateFailure) { if (animateFailure) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt index 0a5f5d281f83..a78c2c0d16c6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt @@ -72,10 +72,7 @@ internal fun PatternBouncer( centerDotsVertically: Boolean, modifier: Modifier = Modifier, ) { - DisposableEffect(Unit) { - viewModel.onShown() - onDispose { viewModel.onHidden() } - } + DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } val colCount = viewModel.columnCount val rowCount = viewModel.rowCount diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt index f505b9067140..5651a4646b2d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt @@ -72,10 +72,7 @@ fun PinPad( verticalSpacing: Dp, modifier: Modifier = Modifier, ) { - DisposableEffect(Unit) { - viewModel.onShown() - onDispose { viewModel.onHidden() } - } + DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState() diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index d0c498475d0b..a1d8c29c2a39 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -71,6 +71,7 @@ fun CommunalContainer( currentScene, onChangeScene = { viewModel.onSceneChanged(it) }, transitions = sceneTransitions, + enableInterruptions = false, ) val touchesAllowed by viewModel.touchesAllowed.collectAsState(initial = false) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt index bc4e55505579..1178cc843d60 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt @@ -74,6 +74,7 @@ constructor( transitions = transitions { sceneKeyByBlueprintId.values.forEach { sceneKey -> to(sceneKey) } }, modifier = modifier, + enableInterruptions = false, ) { sceneKeyByBlueprint.entries.forEach { (blueprint, sceneKey) -> scene(sceneKey) { with(blueprint) { Content(Modifier.fillMaxSize()) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt index d9ed4976bb34..a12f0990b581 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt @@ -80,5 +80,14 @@ object ClockScenes { object ClockElementKeys { val largeClockElementKey = ElementKey("large-clock") val smallClockElementKey = ElementKey("small-clock") + val weatherSmallClockElementKey = ElementKey("weather-small-clock") val smartspaceElementKey = ElementKey("smart-space") } + +object WeatherClockElementKeys { + val timeElementKey = ElementKey("weather-large-clock-time") + val dateElementKey = ElementKey("weather-large-clock-date") + val weatherIconElementKey = ElementKey("weather-large-clock-weather-icon") + val temperatureElementKey = ElementKey("weather-large-clock-temperature") + val dndAlarmElementKey = ElementKey("weather-large-clock-dnd-alarm") +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt index ee4e2d697833..fe774a0d6db2 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt @@ -23,6 +23,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout @@ -33,11 +35,14 @@ import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.padding +import com.android.keyguard.KeyguardClockSwitch.LARGE import com.android.systemui.Flags +import com.android.systemui.customization.R as customizationR import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor.Companion.WEATHER_CLOCK_BLUEPRINT_ID import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.LockscreenLongPress +import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection import com.android.systemui.keyguard.ui.composable.section.LockSection @@ -47,8 +52,8 @@ import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection import com.android.systemui.keyguard.ui.composable.section.StatusBarSection import com.android.systemui.keyguard.ui.composable.section.WeatherClockSection +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel -import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.res.R import com.android.systemui.shade.LargeScreenHeaderHelper import dagger.Binds @@ -71,6 +76,7 @@ constructor( private val settingsMenuSection: SettingsMenuSection, private val clockInteractor: KeyguardClockInteractor, private val mediaCarouselSection: MediaCarouselSection, + private val clockViewModel: KeyguardClockViewModel, ) : ComposableLockscreenSceneBlueprint { override val id: String = WEATHER_CLOCK_BLUEPRINT_ID @@ -79,7 +85,7 @@ constructor( val isUdfpsVisible = viewModel.isUdfpsVisible val burnIn = rememberBurnIn(clockInteractor) val resources = LocalContext.current.resources - + val currentClockState = clockViewModel.currentClock.collectAsState() LockscreenLongPress( viewModel = viewModel.longPress, modifier = modifier, @@ -91,7 +97,34 @@ constructor( modifier = Modifier.fillMaxWidth(), ) { with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) } - // TODO: Add weather clock for small and large clock + val currentClock = currentClockState.value + val clockSize by clockViewModel.clockSize.collectAsState() + with(weatherClockSection) { + if (currentClock == null) { + return@with + } + + if (clockSize == LARGE) { + Time( + clock = currentClock, + modifier = + Modifier.padding( + start = + dimensionResource( + customizationR.dimen.clock_padding_start + ) + ) + ) + } else { + SmallClock( + burnInParams = burnIn.parameters, + modifier = + Modifier.align(Alignment.Start) + .onTopPlacementChanged(burnIn.onSmallClockTopChanged), + clock = currentClock + ) + } + } with(smartSpaceSection) { SmartSpace( burnInParams = burnIn.parameters, @@ -119,6 +152,12 @@ constructor( ) } } + with(weatherClockSection) { + if (currentClock == null || clockSize != LARGE) { + return@with + } + LargeClockSectionBelowSmartspace(clock = currentClock) + } if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { with(ambientIndicationSectionOptional.get()) { @@ -234,6 +273,7 @@ constructor( private val largeScreenHeaderHelper: LargeScreenHeaderHelper, private val weatherClockSection: WeatherClockSection, private val mediaCarouselSection: MediaCarouselSection, + private val clockViewModel: KeyguardClockViewModel, ) : ComposableLockscreenSceneBlueprint { override val id: String = SPLIT_SHADE_WEATHER_CLOCK_BLUEPRINT_ID @@ -242,7 +282,7 @@ constructor( val isUdfpsVisible = viewModel.isUdfpsVisible val burnIn = rememberBurnIn(clockInteractor) val resources = LocalContext.current.resources - + val currentClockState = clockViewModel.currentClock.collectAsState() LockscreenLongPress( viewModel = viewModel.longPress, modifier = modifier, @@ -257,11 +297,42 @@ constructor( Row( modifier = Modifier.fillMaxSize(), ) { - // TODO: Add weather clock for small and large clock Column( modifier = Modifier.fillMaxHeight().weight(weight = 1f), horizontalAlignment = Alignment.CenterHorizontally, ) { + val currentClock = currentClockState.value + val clockSize by clockViewModel.clockSize.collectAsState() + with(weatherClockSection) { + if (currentClock == null) { + return@with + } + + if (clockSize == LARGE) { + Time( + clock = currentClock, + modifier = + Modifier.align(Alignment.Start) + .padding( + start = + dimensionResource( + customizationR.dimen + .clock_padding_start + ) + ) + ) + } else { + SmallClock( + burnInParams = burnIn.parameters, + modifier = + Modifier.align(Alignment.Start) + .onTopPlacementChanged( + burnIn.onSmallClockTopChanged + ), + clock = currentClock, + ) + } + } with(smartSpaceSection) { SmartSpace( burnInParams = burnIn.parameters, @@ -284,6 +355,14 @@ constructor( } with(mediaCarouselSection) { MediaCarousel() } + + with(weatherClockSection) { + if (currentClock == null || clockSize != LARGE) { + return@with + } + + LargeClockSectionBelowSmartspace(currentClock) + } } with(notificationSection) { val splitShadeTopMargin: Dp = diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt index 82e19e7c154c..2781f39fc479 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt @@ -20,6 +20,7 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -32,6 +33,7 @@ import androidx.core.view.contains import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.padding import com.android.systemui.customization.R as customizationR +import com.android.systemui.customization.R import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.largeClockElementKey import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.smallClockElementKey import com.android.systemui.keyguard.ui.composable.modifier.burnInAware @@ -58,20 +60,21 @@ constructor( if (currentClock?.smallClock?.view == null) { return } - viewModel.clock = currentClock - val context = LocalContext.current MovableElement(key = smallClockElementKey, modifier = modifier) { content { AndroidView( factory = { context -> FrameLayout(context).apply { - addClockView(checkNotNull(currentClock).smallClock.view) + ensureClockViewExists(checkNotNull(currentClock).smallClock.view) } }, - update = { it.addClockView(checkNotNull(currentClock).smallClock.view) }, + update = { + it.ensureClockViewExists(checkNotNull(currentClock).smallClock.view) + }, modifier = - Modifier.padding( + Modifier.height(dimensionResource(R.dimen.small_clock_height)) + .padding( horizontal = dimensionResource(customizationR.dimen.clock_padding_start) ) @@ -89,27 +92,27 @@ constructor( @Composable fun SceneScope.LargeClock(modifier: Modifier = Modifier) { val currentClock by viewModel.currentClock.collectAsState() - viewModel.clock = currentClock if (currentClock?.largeClock?.view == null) { return } - MovableElement(key = largeClockElementKey, modifier = modifier) { content { AndroidView( factory = { context -> FrameLayout(context).apply { - addClockView(checkNotNull(currentClock).largeClock.view) + ensureClockViewExists(checkNotNull(currentClock).largeClock.view) } }, - update = { it.addClockView(checkNotNull(currentClock).largeClock.view) }, + update = { + it.ensureClockViewExists(checkNotNull(currentClock).largeClock.view) + }, modifier = Modifier.fillMaxSize() ) } } } - private fun FrameLayout.addClockView(clockView: View) { + private fun FrameLayout.ensureClockViewExists(clockView: View) { if (contains(clockView)) { return } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt index 31d3fa0be163..9f02201f1d81 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt @@ -32,12 +32,12 @@ import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines import com.android.systemui.keyguard.ui.view.DeviceEntryIconView @@ -69,7 +69,7 @@ constructor( ) { @Composable fun SceneScope.LockIcon(modifier: Modifier = Modifier) { - if (!keyguardBottomAreaRefactor() && !DeviceEntryUdfpsRefactor.isEnabled) { + if (!KeyguardBottomAreaRefactor.isEnabled && !DeviceEntryUdfpsRefactor.isEnabled) { return } @@ -96,7 +96,7 @@ constructor( ) } } else { - // keyguardBottomAreaRefactor() + // KeyguardBottomAreaRefactor.isEnabled LockIconView(context, null).apply { id = R.id.lock_icon_view lockIconViewController.get().setLockIconView(this) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 5c9b271b342c..6b86a484069b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -16,50 +16,34 @@ package com.android.systemui.keyguard.ui.composable.section -import android.content.Context import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.notifications.ui.composable.NotificationStack -import com.android.systemui.scene.shared.flag.SceneContainerFlags -import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher @SysUISingleton class NotificationSection @Inject constructor( - @Application private val context: Context, private val viewModel: NotificationsPlaceholderViewModel, - controller: NotificationStackScrollLayoutController, - sceneContainerFlags: SceneContainerFlags, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, stackScrollLayout: NotificationStackScrollLayout, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - @Main private val mainImmediateDispatcher: CoroutineDispatcher, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, ) { init { - if (!migrateClocksToBlueprint()) { - throw IllegalStateException("this requires migrateClocksToBlueprint()") + if (!MigrateClocksToBlueprint.isEnabled) { + throw IllegalStateException("this requires MigrateClocksToBlueprint.isEnabled") } // This scene container section moves the NSSL to the SharedNotificationContainer. // This also requires that SharedNotificationContainer gets moved to the @@ -73,25 +57,10 @@ constructor( sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout) } - SharedNotificationContainerBinder.bind( + sharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainImmediateDispatcher = mainImmediateDispatcher, ) - - if (sceneContainerFlags.isEnabled()) { - NotificationStackAppearanceViewBinder.bind( - context, - sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, - controller, - mainImmediateDispatcher = mainImmediateDispatcher, - ) - } } @Composable diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index 763584182c97..d72d5cad31b4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -92,6 +92,7 @@ constructor( currentScene = currentScene, onChangeScene = {}, transitions = ClockTransition.defaultClockTransitions, + enableInterruptions = false, ) { scene(ClockScenes.splitShadeLargeClockScene) { Row( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt index 2e7bc2a28c65..d3584539b3fa 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/WeatherClockSection.kt @@ -16,45 +16,177 @@ package com.android.systemui.keyguard.ui.composable.section +import android.view.ViewGroup +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.viewinterop.AndroidView +import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.padding +import com.android.systemui.customization.R +import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.weatherSmallClockElementKey +import com.android.systemui.keyguard.ui.composable.blueprint.WeatherClockElementKeys +import com.android.systemui.keyguard.ui.composable.modifier.burnInAware +import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import com.android.systemui.plugins.clocks.ClockController import javax.inject.Inject /** Provides small clock and large clock composables for the weather clock layout. */ -class WeatherClockSection @Inject constructor() { +class WeatherClockSection +@Inject +constructor( + private val viewModel: KeyguardClockViewModel, + private val aodBurnInViewModel: AodBurnInViewModel, +) { @Composable fun SceneScope.Time( + clock: ClockController, modifier: Modifier = Modifier, ) { - // TODO: compose view + WeatherElement( + weatherClockElementViewId = R.id.weather_clock_time, + clock = clock, + elementKey = WeatherClockElementKeys.timeElementKey, + modifier = modifier.wrapContentSize(), + ) } @Composable - fun SceneScope.Date( + private fun SceneScope.Date( + clock: ClockController, modifier: Modifier = Modifier, ) { - // TODO: compose view + WeatherElement( + weatherClockElementViewId = R.id.weather_clock_date, + clock = clock, + elementKey = WeatherClockElementKeys.dateElementKey, + modifier = modifier, + ) } @Composable - fun SceneScope.Weather( + private fun SceneScope.Weather( + clock: ClockController, modifier: Modifier = Modifier, ) { - // TODO: compose view + WeatherElement( + weatherClockElementViewId = R.id.weather_clock_weather_icon, + clock = clock, + elementKey = WeatherClockElementKeys.weatherIconElementKey, + modifier = modifier.wrapContentSize(), + ) } @Composable - fun SceneScope.DndAlarmStatus( + private fun SceneScope.DndAlarmStatus( + clock: ClockController, modifier: Modifier = Modifier, ) { - // TODO: compose view + WeatherElement( + weatherClockElementViewId = R.id.weather_clock_alarm_dnd, + clock = clock, + elementKey = WeatherClockElementKeys.dndAlarmElementKey, + modifier = modifier.wrapContentSize(), + ) } @Composable - fun SceneScope.Temperature( + private fun SceneScope.Temperature( + clock: ClockController, modifier: Modifier = Modifier, ) { - // TODO: compose view + WeatherElement( + weatherClockElementViewId = R.id.weather_clock_temperature, + clock = clock, + elementKey = WeatherClockElementKeys.temperatureElementKey, + modifier = modifier.wrapContentSize(), + ) + } + + @Composable + private fun SceneScope.WeatherElement( + weatherClockElementViewId: Int, + clock: ClockController, + elementKey: ElementKey, + modifier: Modifier + ) { + MovableElement(key = elementKey, modifier) { + content { + AndroidView( + factory = { + val view = + clock.largeClock.layout.views.first { + it.id == weatherClockElementViewId + } + (view.parent as? ViewGroup)?.removeView(view) + view + }, + update = {}, + modifier = modifier + ) + } + } + } + + @Composable + fun SceneScope.LargeClockSectionBelowSmartspace( + clock: ClockController, + ) { + Row( + modifier = + Modifier.height(IntrinsicSize.Max) + .padding(horizontal = dimensionResource(R.dimen.clock_padding_start)) + ) { + Date(clock = clock, modifier = Modifier.wrapContentSize()) + Box(modifier = Modifier.fillMaxSize()) { + Weather(clock = clock, modifier = Modifier.align(Alignment.TopStart)) + Temperature(clock = clock, modifier = Modifier.align(Alignment.BottomEnd)) + DndAlarmStatus(clock = clock, modifier = Modifier.align(Alignment.TopEnd)) + } + } + } + + @Composable + fun SceneScope.SmallClock( + burnInParams: BurnInParameters, + modifier: Modifier = Modifier, + clock: ClockController, + ) { + val localContext = LocalContext.current + MovableElement(key = weatherSmallClockElementKey, modifier) { + content { + AndroidView( + factory = { + val view = clock.smallClock.view + if (view.parent != null) { + (view.parent as? ViewGroup)?.removeView(view) + } + view + }, + modifier = + modifier + .height(dimensionResource(R.dimen.small_clock_height)) + .padding(start = dimensionResource(R.dimen.clock_padding_start)) + .padding(top = { viewModel.getSmallClockTopMargin(localContext) }) + .burnInAware( + viewModel = aodBurnInViewModel, + params = burnInParams, + ), + update = {}, + ) + } + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index d78097815b5e..9ba5e3b846ed 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -57,6 +57,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize @@ -70,9 +71,10 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadi import com.android.systemui.notifications.ui.composable.Notifications.Form import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.ui.composable.ShadeHeader -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS +import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt @@ -139,6 +141,7 @@ fun SceneScope.NotificationScrollingStack( ) { val density = LocalDensity.current val screenCornerRadius = LocalScreenCornerRadius.current + val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius) val scrollState = rememberScrollState() val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f) val expansionFraction by viewModel.expandFraction.collectAsState(0f) @@ -156,6 +159,8 @@ fun SceneScope.NotificationScrollingStack( val contentHeight = viewModel.intrinsicContentHeight.collectAsState() + val stackRounding = viewModel.stackRounding.collectAsState(StackRounding()) + // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is // calculated in minScrimOffset. The scrim is the same height as the screen minus the // height of the Shade Header, and at rest (scrimOffset = 0) its top bound is at maxScrimStartY. @@ -222,16 +227,12 @@ fun SceneScope.NotificationScrollingStack( .graphicsLayer { shape = calculateCornerRadius( + scrimCornerRadius, screenCornerRadius, { expansionFraction }, layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) ) - .let { - RoundedCornerShape( - topStart = it, - topEnd = it, - ) - } + .let { stackRounding.value.toRoundedCornerShape(it) } clip = true } ) { @@ -359,6 +360,7 @@ private fun SceneScope.NotificationPlaceholder( } private fun calculateCornerRadius( + scrimCornerRadius: Dp, screenCornerRadius: Dp, expansionFraction: () -> Float, transitioning: Boolean, @@ -366,12 +368,12 @@ private fun calculateCornerRadius( return if (transitioning) { lerp( start = screenCornerRadius.value, - stop = SCRIM_CORNER_RADIUS, - fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceAtMost(1f), + stop = scrimCornerRadius.value, + fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceIn(0f, 1f), ) .dp } else { - SCRIM_CORNER_RADIUS.dp + scrimCornerRadius } } @@ -394,5 +396,16 @@ private fun Modifier.debugBackground( this } +fun StackRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape { + val topRadius = if (roundTop) radius else 0.dp + val bottomRadius = if (roundBottom) radius else 0.dp + return RoundedCornerShape( + topStart = topRadius, + topEnd = topRadius, + bottomStart = bottomRadius, + bottomEnd = bottomRadius, + ) +} + private const val TAG = "FlexiNotifs" private val DEBUG_COLOR = Color(1f, 0f, 0f, 0.2f) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt index bc48dd1d431f..244861c277c6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -36,7 +37,8 @@ import com.android.compose.modifiers.thenIf import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Expanding -import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Unsquishing +import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQQS +import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQS import com.android.systemui.scene.shared.model.Scenes object QuickSettings { @@ -49,6 +51,8 @@ object QuickSettings { object Elements { val Content = ElementKey("QuickSettingsContent", scenePicker = MovableElementScenePicker(SCENES)) + val QuickQuickSettings = ElementKey("QuickQuickSettings") + val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings") val FooterActions = ElementKey("QuickSettingsFooterActions") } @@ -78,12 +82,16 @@ private fun SceneScope.stateForQuickSettingsContent( is TransitionState.Transition -> with(transitionState) { when { - isSplitShade -> QSSceneAdapter.State.QS - fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> + isSplitShade -> UnsquishingQS(squishiness) + fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> { Expanding(progress) - fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> + } + fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> { Collapsing(progress) - fromScene == Scenes.Shade || toScene == Scenes.Shade -> Unsquishing(squishiness) + } + fromScene == Scenes.Shade || toScene == Scenes.Shade -> { + UnsquishingQQS(squishiness) + } fromScene == Scenes.QuickSettings || toScene == Scenes.QuickSettings -> { QSSceneAdapter.State.QS } @@ -119,6 +127,18 @@ fun SceneScope.QuickSettings( squishiness: Float = QuickSettings.SharedValues.SquishinessValues.Default, ) { val contentState = stateForQuickSettingsContent(isSplitShade, squishiness) + val transitionState = layoutState.transitionState + val isClosing = + transitionState is TransitionState.Transition && + transitionState.progress >= 0.9f && // almost done closing + !(layoutState.isTransitioning(to = Scenes.Shade) || + layoutState.isTransitioning(to = Scenes.QuickSettings)) + + if (isClosing) { + DisposableEffect(Unit) { + onDispose { qsSceneAdapter.setState(QSSceneAdapter.State.CLOSED) } + } + } MovableElement( key = QuickSettings.Elements.Content, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index 0fdaabe75306..fe6701cc8d89 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -79,6 +79,7 @@ fun SceneContainer( initialScene = currentSceneKey, canChangeScene = { toScene -> viewModel.canChangeScene(toScene) }, transitions = SceneContainerTransitions, + enableInterruptions = false, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt index 5c6e1c89ad65..9b59708fe81d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt @@ -13,11 +13,18 @@ fun TransitionBuilder.goneToShadeTransition( ) { spec = tween(durationMillis = DefaultDuration.times(durationScale).inWholeMilliseconds.toInt()) - fractionRange(start = .58f) { fade(ShadeHeader.Elements.Clock) } - fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentStart) } - fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentEnd) } - fractionRange(start = .58f) { fade(ShadeHeader.Elements.PrivacyChip) } - translate(QuickSettings.Elements.Content, y = -ShadeHeader.Dimensions.CollapsedHeight * .66f) + fractionRange(start = .58f) { + fade(ShadeHeader.Elements.Clock) + fade(ShadeHeader.Elements.CollapsedContentStart) + fade(ShadeHeader.Elements.CollapsedContentEnd) + fade(ShadeHeader.Elements.PrivacyChip) + fade(QuickSettings.Elements.SplitShadeQuickSettings) + fade(QuickSettings.Elements.FooterActions) + } + translate( + QuickSettings.Elements.QuickQuickSettings, + y = -ShadeHeader.Dimensions.CollapsedHeight * .66f + ) translate(Notifications.Elements.NotificationScrim, Edge.Top, false) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 15e7b511915e..85798acd0dcd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexScenePicker import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.TransitionState import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneFloatAsState @@ -222,15 +223,17 @@ private fun SceneScope.SingleShade( horizontal = Shade.Dimensions.HorizontalPadding ) ) - QuickSettings( - viewModel.qsSceneAdapter, - { - (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness) - .roundToInt() - }, - isSplitShade = false, - squishiness = tileSquishiness, - ) + Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) { + QuickSettings( + viewModel.qsSceneAdapter, + { + (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness) + .roundToInt() + }, + isSplitShade = false, + squishiness = tileSquishiness, + ) + } MediaIfVisible( viewModel = viewModel, @@ -280,6 +283,8 @@ private fun SceneScope.SplitShade( val lifecycleOwner = LocalLifecycleOwner.current val footerActionsViewModel = remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) } + val tileSquishiness by + animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness) val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val density = LocalDensity.current @@ -290,6 +295,7 @@ private fun SceneScope.SplitShade( } val quickSettingsScrollState = rememberScrollState() + val isScrollable = layoutState.transitionState is TransitionState.Idle LaunchedEffect(isCustomizing, quickSettingsScrollState) { if (isCustomizing) { quickSettingsScrollState.scrollTo(0) @@ -318,31 +324,41 @@ private fun SceneScope.SplitShade( Column( verticalArrangement = Arrangement.Top, modifier = - Modifier.weight(1f).fillMaxHeight().thenIf(!isCustomizing) { - Modifier.verticalNestedScrollToScene() - .verticalScroll(quickSettingsScrollState) - .clipScrollableContainer(Orientation.Horizontal) - .padding(bottom = navBarBottomHeight) - } + Modifier.weight(1f).fillMaxSize().thenIf(!isCustomizing) { + Modifier.padding(bottom = navBarBottomHeight) + }, ) { - QuickSettings( - qsSceneAdapter = viewModel.qsSceneAdapter, - heightProvider = { viewModel.qsSceneAdapter.qsHeight }, - isSplitShade = true, - modifier = Modifier.fillMaxWidth(), - ) - - MediaIfVisible( - viewModel = viewModel, - mediaCarouselController = mediaCarouselController, - mediaHost = mediaHost, - modifier = Modifier.fillMaxWidth(), - ) - - Spacer( - modifier = Modifier.weight(1f), - ) + Column( + modifier = + Modifier.fillMaxSize().weight(1f).thenIf(!isCustomizing) { + Modifier.verticalNestedScrollToScene() + .verticalScroll( + quickSettingsScrollState, + enabled = isScrollable + ) + .clipScrollableContainer(Orientation.Horizontal) + } + ) { + Box( + modifier = + Modifier.element(QuickSettings.Elements.SplitShadeQuickSettings) + ) { + QuickSettings( + qsSceneAdapter = viewModel.qsSceneAdapter, + heightProvider = { viewModel.qsSceneAdapter.qsHeight }, + isSplitShade = true, + modifier = Modifier.fillMaxWidth(), + squishiness = tileSquishiness, + ) + } + MediaIfVisible( + viewModel = viewModel, + mediaCarouselController = mediaCarouselController, + mediaHost = mediaHost, + modifier = Modifier.fillMaxWidth(), + ) + } FooterActionsWithAnimatedVisibility( viewModel = footerActionsViewModel, isCustomizing = isCustomizing, @@ -354,7 +370,8 @@ private fun SceneScope.SplitShade( NotificationScrollingStack( viewModel = viewModel.notifications, maxScrimTop = { 0f }, - modifier = Modifier.weight(1f).fillMaxHeight(), + modifier = + Modifier.weight(1f).fillMaxHeight().padding(bottom = navBarBottomHeight), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/selector/ui/composable/VolumePanelRadioButtons.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/selector/ui/composable/VolumePanelRadioButtons.kt index ae267e2b002a..98d1afd7af7e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/selector/ui/composable/VolumePanelRadioButtons.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/selector/ui/composable/VolumePanelRadioButtons.kt @@ -16,38 +16,38 @@ package com.android.systemui.volume.panel.component.selector.ui.composable -import androidx.compose.animation.core.animateOffsetAsState -import androidx.compose.foundation.Canvas +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier -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.layout.onGloballyPositioned +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastFirst +import kotlinx.coroutines.launch /** * Radio button group for the Volume Panel. It allows selecting a single item @@ -65,8 +65,8 @@ fun VolumePanelRadioButtonBar( spacing: Dp = VolumePanelRadioButtonBarDefaults.DefaultSpacing, labelIndicatorBackgroundSpacing: Dp = VolumePanelRadioButtonBarDefaults.DefaultLabelIndicatorBackgroundSpacing, - indicatorCornerRadius: CornerRadius = - VolumePanelRadioButtonBarDefaults.defaultIndicatorCornerRadius(), + indicatorCornerSize: CornerSize = + CornerSize(VolumePanelRadioButtonBarDefaults.DefaultIndicatorCornerRadius), indicatorBackgroundCornerSize: CornerSize = CornerSize(VolumePanelRadioButtonBarDefaults.DefaultIndicatorBackgroundCornerRadius), colors: VolumePanelRadioButtonBarColors = VolumePanelRadioButtonBarDefaults.defaultColors(), @@ -76,60 +76,41 @@ fun VolumePanelRadioButtonBar( VolumePanelRadioButtonBarScopeImpl().apply(content).apply { require(hasSelectedItem) { "At least one item should be selected" } } - val items = scope.items - var selectedIndex by remember { mutableIntStateOf(items.indexOfFirst { it.isSelected }) } - - var size by remember { mutableStateOf(IntSize(0, 0)) } - val spacingPx = with(LocalDensity.current) { spacing.toPx() } - val indicatorWidth = size.width / items.size - (spacingPx * (items.size - 1) / items.size) - val offset by - animateOffsetAsState( - targetValue = - Offset( - selectedIndex * indicatorWidth + (spacingPx * selectedIndex), - 0f, - ), - label = "VolumePanelRadioButtonOffsetAnimation", - finishedListener = { - for (itemIndex in items.indices) { - val item = items[itemIndex] - if (itemIndex == selectedIndex) { - item.onItemSelected() - break - } - } - } - ) - - Column(modifier = modifier) { - Box(modifier = Modifier.height(IntrinsicSize.Max)) { - Canvas( + val coroutineScope = rememberCoroutineScope() + val offsetAnimatable = remember { Animatable(UNSET_OFFSET, Int.VectorConverter) } + Layout( + modifier = modifier, + content = { + Spacer( modifier = - Modifier.fillMaxSize() + Modifier.layoutId(RadioButtonBarComponent.ButtonsBackground) .background( colors.indicatorBackgroundColor, RoundedCornerShape(indicatorBackgroundCornerSize), ) + ) + Spacer( + modifier = + Modifier.layoutId(RadioButtonBarComponent.Indicator) + .offset { IntOffset(offsetAnimatable.value, 0) } .padding(indicatorBackgroundPadding) - .onGloballyPositioned { size = it.size } - ) { - drawRoundRect( - color = colors.indicatorColor, - topLeft = offset, - size = Size(indicatorWidth, size.height.toFloat()), - cornerRadius = indicatorCornerRadius, - ) - } + .background( + colors.indicatorColor, + RoundedCornerShape(indicatorCornerSize), + ) + ) Row( - modifier = Modifier.padding(indicatorBackgroundPadding), + modifier = + Modifier.layoutId(RadioButtonBarComponent.Buttons) + .padding(indicatorBackgroundPadding), horizontalArrangement = Arrangement.spacedBy(spacing) ) { for (itemIndex in items.indices) { TextButton( modifier = Modifier.weight(1f), - onClick = { selectedIndex = itemIndex }, + onClick = { items[itemIndex].onItemSelected() }, ) { val item = items[itemIndex] if (item.icon !== Empty) { @@ -138,28 +119,116 @@ fun VolumePanelRadioButtonBar( } } } - } + Row( + modifier = + Modifier.layoutId(RadioButtonBarComponent.Labels) + .padding( + start = indicatorBackgroundPadding, + top = labelIndicatorBackgroundSpacing, + end = indicatorBackgroundPadding + ), + horizontalArrangement = Arrangement.spacedBy(spacing), + ) { + for (itemIndex in items.indices) { + TextButton( + modifier = Modifier.weight(1f), + onClick = { items[itemIndex].onItemSelected() }, + ) { + val item = items[itemIndex] + if (item.icon !== Empty) { + with(items[itemIndex]) { label() } + } + } + } + } + }, + measurePolicy = + with(LocalDensity.current) { + val spacingPx = + (spacing - indicatorBackgroundPadding * 2).roundToPx().coerceAtLeast(0) - Row( - modifier = - Modifier.padding( - start = indicatorBackgroundPadding, - top = labelIndicatorBackgroundSpacing, - end = indicatorBackgroundPadding - ), - horizontalArrangement = Arrangement.spacedBy(spacing), - ) { - for (itemIndex in items.indices) { - TextButton( - modifier = Modifier.weight(1f), - onClick = { selectedIndex = itemIndex }, + BarMeasurePolicy( + buttonsCount = items.size, + selectedIndex = scope.selectedIndex, + spacingPx = spacingPx, ) { - val item = items[itemIndex] - if (item.icon !== Empty) { - with(items[itemIndex]) { label() } + coroutineScope.launch { + if (offsetAnimatable.value == UNSET_OFFSET) { + offsetAnimatable.snapTo(it) + } else { + offsetAnimatable.animateTo(it) + } } } - } + }, + ) +} + +private class BarMeasurePolicy( + private val buttonsCount: Int, + private val selectedIndex: Int, + private val spacingPx: Int, + private val onTargetIndicatorOffsetMeasured: (Int) -> Unit, +) : MeasurePolicy { + + override fun MeasureScope.measure( + measurables: List<Measurable>, + constraints: Constraints + ): MeasureResult { + val fillWidthConstraints = constraints.copy(minWidth = constraints.maxWidth) + val buttonsPlaceable: Placeable = + measurables + .fastFirst { it.layoutId == RadioButtonBarComponent.Buttons } + .measure(fillWidthConstraints) + val labelsPlaceable: Placeable = + measurables + .fastFirst { it.layoutId == RadioButtonBarComponent.Labels } + .measure(fillWidthConstraints) + + val buttonsBackgroundPlaceable: Placeable = + measurables + .fastFirst { it.layoutId == RadioButtonBarComponent.ButtonsBackground } + .measure( + Constraints( + minWidth = buttonsPlaceable.width, + maxWidth = buttonsPlaceable.width, + minHeight = buttonsPlaceable.height, + maxHeight = buttonsPlaceable.height, + ) + ) + + val totalSpacing = spacingPx * (buttonsCount - 1) + val indicatorWidth = (buttonsBackgroundPlaceable.width - totalSpacing) / buttonsCount + val indicatorPlaceable: Placeable = + measurables + .fastFirst { it.layoutId == RadioButtonBarComponent.Indicator } + .measure( + Constraints( + minWidth = indicatorWidth, + maxWidth = indicatorWidth, + minHeight = buttonsBackgroundPlaceable.height, + maxHeight = buttonsBackgroundPlaceable.height, + ) + ) + + onTargetIndicatorOffsetMeasured( + selectedIndex * indicatorWidth + (spacingPx * selectedIndex) + ) + + return layout(constraints.maxWidth, buttonsPlaceable.height + labelsPlaceable.height) { + buttonsBackgroundPlaceable.placeRelative( + 0, + 0, + RadioButtonBarComponent.ButtonsBackground.zIndex, + ) + indicatorPlaceable.placeRelative(0, 0, RadioButtonBarComponent.Indicator.zIndex) + + buttonsPlaceable.placeRelative(0, 0, RadioButtonBarComponent.Buttons.zIndex) + labelsPlaceable.placeRelative( + 0, + buttonsBackgroundPlaceable.height, + RadioButtonBarComponent.Labels.zIndex, + ) } } } @@ -179,12 +248,6 @@ object VolumePanelRadioButtonBarDefaults { val DefaultIndicatorCornerRadius = 20.dp val DefaultIndicatorBackgroundCornerRadius = 20.dp - @Composable - fun defaultIndicatorCornerRadius( - x: Dp = DefaultIndicatorCornerRadius, - y: Dp = DefaultIndicatorCornerRadius, - ): CornerRadius = with(LocalDensity.current) { CornerRadius(x.toPx(), y.toPx()) } - /** * Returns the default VolumePanelRadioButtonBar colors. * @@ -225,9 +288,12 @@ private val Empty: @Composable RowScope.() -> Unit = {} private class VolumePanelRadioButtonBarScopeImpl : VolumePanelRadioButtonBarScope { - var hasSelectedItem: Boolean = false + var selectedIndex: Int = UNSET_INDEX private set + val hasSelectedItem: Boolean + get() = selectedIndex != UNSET_INDEX + private val mutableItems: MutableList<Item> = mutableListOf() val items: List<Item> = mutableItems @@ -238,21 +304,34 @@ private class VolumePanelRadioButtonBarScopeImpl : VolumePanelRadioButtonBarScop label: @Composable RowScope.() -> Unit, ) { require(!isSelected || !hasSelectedItem) { "Only one item should be selected at a time" } - hasSelectedItem = hasSelectedItem || isSelected + if (isSelected) { + selectedIndex = mutableItems.size + } mutableItems.add( Item( - isSelected = isSelected, onItemSelected = onItemSelected, icon = icon, label = label, ) ) } + + private companion object { + const val UNSET_INDEX = -1 + } } private class Item( - val isSelected: Boolean, val onItemSelected: () -> Unit, val icon: @Composable RowScope.() -> Unit, val label: @Composable RowScope.() -> Unit, ) + +private const val UNSET_OFFSET = -1 + +private enum class RadioButtonBarComponent(val zIndex: Float) { + ButtonsBackground(0f), + Indicator(1f), + Buttons(2f), + Labels(2f), +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt index bed0ae80e377..71b3e8a6a102 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt @@ -65,7 +65,7 @@ constructor( return } - val enabledModelStates by viewModel.spatialAudioButtonByEnabled.collectAsState() + val enabledModelStates by viewModel.spatialAudioButtons.collectAsState() if (enabledModelStates.isEmpty()) { return } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index 24351706cb46..d31064ae23b3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -17,16 +17,20 @@ package com.android.systemui.volume.panel.component.volume.ui.composable import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonColors import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.ProgressBarRangeInfo @@ -38,6 +42,7 @@ import androidx.compose.ui.semantics.setProgress import androidx.compose.ui.unit.dp import com.android.compose.PlatformSlider import com.android.compose.PlatformSliderColors +import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState @@ -49,16 +54,15 @@ fun VolumeSlider( modifier: Modifier = Modifier, sliderColors: PlatformSliderColors, ) { - val value by - animateFloatAsState(targetValue = state.value, label = "VolumeSliderValueAnimation") + val value by valueState(state) PlatformSlider( modifier = modifier.clearAndSetSemantics { if (!state.isEnabled) disabled() contentDescription = state.label - // provide a not animated value to the a11y because it fails to announce the settled - // value when it changes rapidly. + // provide a not animated value to the a11y because it fails to announce the + // settled value when it changes rapidly. progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange) setProgress { targetValue -> val targetDirection = @@ -86,44 +90,64 @@ fun VolumeSlider( Text(text = state.valueText, color = LocalContentColor.current) } else { state.icon?.let { - IconButton( - onClick = onIconTapped, - colors = - IconButtonColors( - contentColor = LocalContentColor.current, - containerColor = Color.Transparent, - disabledContentColor = LocalContentColor.current, - disabledContainerColor = Color.Transparent, - ) - ) { - Icon(modifier = Modifier.size(24.dp), icon = it) - } + SliderIcon( + icon = it, + onIconTapped = onIconTapped, + isTappable = state.isMutable, + ) } } }, colors = sliderColors, label = { - Column(modifier = Modifier) { - Text( - modifier = Modifier.basicMarquee(), - text = state.label, - style = MaterialTheme.typography.titleMedium, - color = LocalContentColor.current, - maxLines = 1, - ) - - if (!state.isEnabled) { - state.disabledMessage?.let { message -> - Text( - modifier = Modifier.basicMarquee(), - text = message, - style = MaterialTheme.typography.bodySmall, - color = LocalContentColor.current, - maxLines = 1, - ) - } - } - } + VolumeSliderContent( + modifier = Modifier, + label = state.label, + isEnabled = state.isEnabled, + disabledMessage = state.disabledMessage, + ) } ) } + +@Composable +private fun valueState(state: SliderState): State<Float> { + var prevState by remember { mutableStateOf(state) } + // Don't animate slider value when receive the first value and when changing isEnabled state + val shouldSkipAnimation = + prevState is SliderState.Empty || prevState.isEnabled != state.isEnabled + val value = + if (shouldSkipAnimation) mutableFloatStateOf(state.value) + else animateFloatAsState(targetValue = state.value, label = "VolumeSliderValueAnimation") + prevState = state + return value +} + +@Composable +private fun SliderIcon( + icon: Icon, + onIconTapped: () -> Unit, + isTappable: Boolean, + modifier: Modifier = Modifier +) { + if (isTappable) { + IconButton( + modifier = modifier, + onClick = onIconTapped, + colors = + IconButtonColors( + contentColor = LocalContentColor.current, + containerColor = Color.Transparent, + disabledContentColor = LocalContentColor.current, + disabledContainerColor = Color.Transparent, + ), + content = { Icon(modifier = Modifier.size(24.dp), icon = icon) }, + ) + } else { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + content = { Icon(modifier = Modifier.size(24.dp), icon = icon) }, + ) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt new file mode 100644 index 000000000000..6b9af239eb6f --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSliderContent.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.volume.panel.component.volume.ui.composable + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationVector1D +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.basicMarquee +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasurePolicy +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.util.fastFirst +import androidx.compose.ui.util.fastFirstOrNull +import kotlinx.coroutines.launch + +private enum class VolumeSliderContentComponent { + Label, + DisabledMessage, +} + +/** Shows label of the [VolumeSlider]. Also shows [disabledMessage] when not [isEnabled]. */ +@Composable +fun VolumeSliderContent( + label: String, + isEnabled: Boolean, + disabledMessage: String?, + modifier: Modifier = Modifier, +) { + Layout( + modifier = modifier.animateContentHeight(), + content = { + Text( + modifier = Modifier.layoutId(VolumeSliderContentComponent.Label).basicMarquee(), + text = label, + style = MaterialTheme.typography.titleMedium, + color = LocalContentColor.current, + maxLines = 1, + ) + + disabledMessage?.let { message -> + AnimatedVisibility( + modifier = Modifier.layoutId(VolumeSliderContentComponent.DisabledMessage), + visible = !isEnabled, + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + ) { + Text( + modifier = Modifier.basicMarquee(), + text = message, + style = MaterialTheme.typography.bodySmall, + color = LocalContentColor.current, + maxLines = 1, + ) + } + } + }, + measurePolicy = VolumeSliderContentMeasurePolicy(isEnabled) + ) +} + +/** + * Uses [VolumeSliderContentComponent.Label] width when [isEnabled] and max available width + * otherwise. This ensures that the slider always have the correct measurement to position the + * content. + */ +private class VolumeSliderContentMeasurePolicy(private val isEnabled: Boolean) : MeasurePolicy { + + override fun MeasureScope.measure( + measurables: List<Measurable>, + constraints: Constraints + ): MeasureResult { + val labelPlaceable = + measurables + .fastFirst { it.layoutId == VolumeSliderContentComponent.Label } + .measure(constraints) + val layoutWidth: Int = constraints.maxWidth + val fullLayoutWidth: Int = + if (isEnabled) { + // PlatformSlider uses half of the available space for the enabled state. + // This is using it to allow disabled message to take whole space when animating to + // prevent it from jumping left to right + layoutWidth * 2 + } else { + layoutWidth + } + + val disabledMessagePlaceable = + measurables + .fastFirstOrNull { it.layoutId == VolumeSliderContentComponent.DisabledMessage } + ?.measure(constraints.copy(maxWidth = fullLayoutWidth)) + + val layoutHeight = labelPlaceable.height + (disabledMessagePlaceable?.height ?: 0) + return layout(layoutWidth, layoutHeight) { + labelPlaceable.placeRelative(0, 0, 0f) + disabledMessagePlaceable?.placeRelative(0, labelPlaceable.height, 0f) + } + } +} + +/** Animates composable height changes. */ +@Composable +private fun Modifier.animateContentHeight(): Modifier { + var heightAnimation by remember { mutableStateOf<Animatable<Int, AnimationVector1D>?>(null) } + val coroutineScope = rememberCoroutineScope() + return layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + val currentAnimation = heightAnimation + val anim = + if (currentAnimation == null) { + Animatable(placeable.height, Int.VectorConverter).also { heightAnimation = it } + } else { + coroutineScope.launch { currentAnimation.animateTo(placeable.height) } + currentAnimation + } + layout(placeable.width, anim.value) { placeable.place(0, 0) } + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index 6cff30cf0369..da07f6d12a67 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -138,8 +138,9 @@ private fun CoroutineScope.animate( // that will actually animate it. layoutState.startTransition(transition, transitionKey) - // The transformation now contains the spec that we should use to instantiate the Animatable. - val animationSpec = layoutState.transformationSpec.progressSpec + // The transition now contains the transformation spec that we should use to instantiate the + // Animatable. + val animationSpec = transition.transformationSpec.progressSpec val visibilityThreshold = (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold val animatable = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 82083f99ba3e..1b0627576af7 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -18,10 +18,8 @@ package com.android.compose.animation.scene -import android.util.Log import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D -import androidx.compose.animation.core.SpringSpec import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -145,16 +143,6 @@ internal class DraggableHandlerImpl( } val transitionState = layoutImpl.state.transitionState - if (transitionState is TransitionState.Transition) { - // TODO(b/290184746): Better handle interruptions here if state != idle. - Log.w( - TAG, - "start from TransitionState.Transition is not fully supported: from" + - " ${transitionState.fromScene} to ${transitionState.toScene} " + - "(progress ${transitionState.progress})" - ) - } - val fromScene = layoutImpl.scene(transitionState.currentScene) val swipes = computeSwipes(fromScene, startedPosition, pointersDown) val result = @@ -269,19 +257,6 @@ private class DragControllerImpl( fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) { if (isDrivingTransition || force) { layoutState.startTransition(newTransition, newTransition.key) - - // Initialize SwipeTransition.transformationSpec and .swipeSpec. Note that this must be - // called right after layoutState.startTransition() is called, because it computes the - // current layoutState.transformationSpec(). - val transformationSpec = layoutState.transformationSpec - newTransition.transformationSpec = transformationSpec - newTransition.swipeSpec = - transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec - } else { - // We were not driving the transition and we don't force the update, so the specs won't - // be used and it doesn't matter which ones we set here. - newTransition.transformationSpec = TransformationSpec.Empty - newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec } swipeTransition = newTransition @@ -616,18 +591,6 @@ private class SwipeTransition( override val isUserInputOngoing: Boolean get() = offsetAnimation == null - /** - * The [TransformationSpecImpl] associated to this transition. - * - * Note: This is lateinit because this [SwipeTransition] is needed by - * [BaseSceneTransitionLayoutState] to compute the [TransitionSpec], and it will be set right - * after [BaseSceneTransitionLayoutState.startTransition] is called with this transition. - */ - lateinit var transformationSpec: TransformationSpecImpl - - /** The spec to use when animating this transition to either [fromScene] or [toScene]. */ - lateinit var swipeSpec: SpringSpec<Float> - override val overscrollScope: OverscrollScope = object : OverscrollScope { override val absoluteDistance: Float @@ -701,6 +664,9 @@ private class SwipeTransition( coroutineScope .launch { try { + val swipeSpec = + transformationSpec.swipeSpec + ?: layoutState.transitions.defaultSwipeSpec animatable.animateTo( targetValue = targetOffset, animationSpec = swipeSpec, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index 15712b5c7206..69f1d456b2fb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -203,7 +203,7 @@ internal class ElementNode( measurable: Measurable, constraints: Constraints, ): MeasureResult { - val overscrollScene = layoutImpl.state.currentOverscrollSpec?.scene + val overscrollScene = layoutImpl.state.currentTransition?.currentOverscrollSpec?.scene if (overscrollScene != null && overscrollScene != scene.key) { // There is an overscroll in progress on another scene // By measuring composable elements, Compose can cache relevant information. @@ -269,13 +269,12 @@ private fun shouldDrawElement( transition == null || transition.fromScene !in element.sceneStates || transition.toScene !in element.sceneStates || - layoutImpl.state.currentOverscrollSpec?.scene == scene.key + transition.currentOverscrollSpec?.scene == scene.key ) { return true } - val sharedTransformation = - sharedElementTransformation(layoutImpl.state, transition, element.key) + val sharedTransformation = sharedElementTransformation(transition, element.key) if (sharedTransformation?.enabled == false) { return true } @@ -305,23 +304,21 @@ internal fun shouldDrawOrComposeSharedElement( fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex, toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex, ) == scene - return chosenByPicker || layoutImpl.state.currentOverscrollSpec?.scene == scene + return chosenByPicker || transition.currentOverscrollSpec?.scene == scene } private fun isSharedElementEnabled( - layoutState: BaseSceneTransitionLayoutState, transition: TransitionState.Transition, element: ElementKey, ): Boolean { - return sharedElementTransformation(layoutState, transition, element)?.enabled ?: true + return sharedElementTransformation(transition, element)?.enabled ?: true } internal fun sharedElementTransformation( - layoutState: BaseSceneTransitionLayoutState, transition: TransitionState.Transition, element: ElementKey, ): SharedElementTransformation? { - val transformationSpec = layoutState.transformationSpec + val transformationSpec = transition.transformationSpec val sharedInFromScene = transformationSpec.transformations(element, transition.fromScene).shared val sharedInToScene = transformationSpec.transformations(element, transition.toScene).shared @@ -360,11 +357,11 @@ private fun isElementOpaque( } val isSharedElement = fromState != null && toState != null - if (isSharedElement && isSharedElementEnabled(layoutImpl.state, transition, element.key)) { + if (isSharedElement && isSharedElementEnabled(transition, element.key)) { return true } - return layoutImpl.state.transformationSpec.transformations(element.key, scene.key).alpha == null + return transition.transformationSpec.transformations(element.key, scene.key).alpha == null } /** @@ -559,7 +556,7 @@ private inline fun <T> computeValue( } if (transition is TransitionState.HasOverscrollProperties) { - val overscroll = layoutImpl.state.currentOverscrollSpec + val overscroll = transition.currentOverscrollSpec if (overscroll?.scene == scene.key) { val elementSpec = overscroll.transformationSpec.transformations(element.key, scene.key) val propertySpec = transformation(elementSpec) ?: return currentValue() @@ -597,7 +594,7 @@ private inline fun <T> computeValue( // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared // elements follow the finger direction. val isSharedElement = fromState != null && toState != null - if (isSharedElement && isSharedElementEnabled(layoutImpl.state, transition, element.key)) { + if (isSharedElement && isSharedElementEnabled(transition, element.key)) { val start = sceneValue(fromState!!) val end = sceneValue(toState!!) @@ -607,7 +604,7 @@ private inline fun <T> computeValue( } val transformation = - transformation(layoutImpl.state.transformationSpec.transformations(element.key, scene.key)) + transformation(transition.transformationSpec.transformations(element.key, scene.key)) // If there is no transformation explicitly associated to this element value, let's use // the value given by the system (like the current position and size given by the layout // pass). diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index af51cee2a255..dc3b612d3594 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -73,7 +73,7 @@ internal class Scene( internal class SceneScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val scene: Scene, -) : SceneScope { +) : SceneScope, ElementStateScope by layoutImpl.elementStateScope { override val layoutState: SceneTransitionLayoutState = layoutImpl.state override fun Modifier.element(key: ElementKey): Modifier { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index b7e2dd13f321..c7c874c1185d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -96,9 +96,17 @@ fun SceneTransitionLayout( modifier: Modifier = Modifier, swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, + enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, scenes: SceneTransitionLayoutScope.() -> Unit, ) { - val state = updateSceneTransitionLayoutState(currentScene, onChangeScene, transitions) + val state = + updateSceneTransitionLayoutState( + currentScene, + onChangeScene, + transitions, + enableInterruptions = enableInterruptions, + ) + SceneTransitionLayout( state, modifier, @@ -131,9 +139,30 @@ interface SceneTransitionLayoutScope { */ @DslMarker annotation class ElementDsl +/** A scope that can be used to query the target state of an element or scene. */ +interface ElementStateScope { + /** + * Return the *target* size of [this] element in the given [scene], i.e. the size of the element + * when idle, or `null` if the element is not composed and measured in that scene (yet). + */ + fun ElementKey.targetSize(scene: SceneKey): IntSize? + + /** + * Return the *target* offset of [this] element in the given [scene], i.e. the size of the + * element when idle, or `null` if the element is not composed and placed in that scene (yet). + */ + fun ElementKey.targetOffset(scene: SceneKey): Offset? + + /** + * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if + * the scene was never composed. + */ + fun SceneKey.targetSize(): IntSize? +} + @Stable @ElementDsl -interface BaseSceneScope { +interface BaseSceneScope : ElementStateScope { /** The state of the [SceneTransitionLayout] in which this scene is contained. */ val layoutState: SceneTransitionLayoutState @@ -415,25 +444,7 @@ interface UserActionDistance { ): Float } -interface UserActionDistanceScope : Density { - /** - * Return the *target* size of [this] element in the given [scene], i.e. the size of the element - * when idle, or `null` if the element is not composed and measured in that scene (yet). - */ - fun ElementKey.targetSize(scene: SceneKey): IntSize? - - /** - * Return the *target* offset of [this] element in the given [scene], i.e. the size of the - * element when idle, or `null` if the element is not composed and placed in that scene (yet). - */ - fun ElementKey.targetOffset(scene: SceneKey): Offset? - - /** - * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if - * the scene was never composed. - */ - fun SceneKey.targetSize(): IntSize? -} +interface UserActionDistanceScope : Density, ElementStateScope /** The user action has a fixed [absoluteDistance]. */ class FixedDistance(private val distance: Dp) : UserActionDistance { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 25b0895fafb3..b1cfdcf07977 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -98,6 +98,7 @@ internal class SceneTransitionLayoutImpl( private val horizontalDraggableHandler: DraggableHandlerImpl private val verticalDraggableHandler: DraggableHandlerImpl + internal val elementStateScope = ElementStateScopeImpl(this) private var _userActionDistanceScope: UserActionDistanceScope? = null internal val userActionDistanceScope: UserActionDistanceScope get() = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 617a8ea0b6cd..f13c016e9d68 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -16,15 +16,16 @@ package com.android.compose.animation.scene +import android.util.Log +import androidx.annotation.VisibleForTesting import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastForEach import com.android.compose.animation.scene.transition.link.LinkedTransition @@ -50,10 +51,21 @@ sealed interface SceneTransitionLayoutState { */ val transitionState: TransitionState - /** The current transition, or `null` if we are idle. */ + /** + * The current transition, or `null` if we are idle. + * + * Note: If you need to handle interruptions and multiple transitions running in parallel, use + * [currentTransitions] instead. + */ val currentTransition: TransitionState.Transition? get() = transitionState as? TransitionState.Transition + /** + * The list of [TransitionState.Transition] currently running. This will be the empty list if we + * are idle. + */ + val currentTransitions: List<TransitionState.Transition> + /** The [SceneTransitions] used when animating this state. */ val transitions: SceneTransitions @@ -120,12 +132,14 @@ fun MutableSceneTransitionLayoutState( transitions: SceneTransitions = SceneTransitions.Empty, canChangeScene: (SceneKey) -> Boolean = { true }, stateLinks: List<StateLink> = emptyList(), + enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, ): MutableSceneTransitionLayoutState { return MutableSceneTransitionLayoutStateImpl( initialScene, transitions, canChangeScene, stateLinks, + enableInterruptions, ) } @@ -154,6 +168,7 @@ fun updateSceneTransitionLayoutState( transitions: SceneTransitions = SceneTransitions.Empty, canChangeScene: (SceneKey) -> Boolean = { true }, stateLinks: List<StateLink> = emptyList(), + enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, ): SceneTransitionLayoutState { return remember { HoistedSceneTransitionLayoutState( @@ -162,9 +177,19 @@ fun updateSceneTransitionLayoutState( onChangeScene, canChangeScene, stateLinks, + enableInterruptions, + ) + } + .apply { + update( + currentScene, + onChangeScene, + canChangeScene, + transitions, + stateLinks, + enableInterruptions, ) } - .apply { update(currentScene, onChangeScene, canChangeScene, transitions, stateLinks) } } @Stable @@ -204,6 +229,30 @@ sealed interface TransitionState { /** Whether user input is currently driving the transition. */ abstract val isUserInputOngoing: Boolean + /** + * The current [TransformationSpecImpl] and [OverscrollSpecImpl] associated to this + * transition. + * + * Important: These will be set exactly once, when this transition is + * [started][BaseSceneTransitionLayoutState.startTransition]. + */ + internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty + private var fromOverscrollSpec: OverscrollSpecImpl? = null + private var toOverscrollSpec: OverscrollSpecImpl? = null + + /** The current [OverscrollSpecImpl], if this transition is currently overscrolling. */ + internal val currentOverscrollSpec: OverscrollSpecImpl? + get() { + if (this !is HasOverscrollProperties) return null + val progress = progress + val bouncingScene = bouncingScene + return when { + progress < 0f || bouncingScene == fromScene -> fromOverscrollSpec + progress > 1f || bouncingScene == toScene -> toOverscrollSpec + else -> null + } + } + init { check(fromScene != toScene) } @@ -232,6 +281,14 @@ sealed interface TransitionState { return isTransitioning(from = scene, to = other) || isTransitioning(from = other, to = scene) } + + internal fun updateOverscrollSpecs( + fromSpec: OverscrollSpecImpl?, + toSpec: OverscrollSpecImpl?, + ) { + fromOverscrollSpec = fromSpec + toOverscrollSpec = toSpec + } } interface HasOverscrollProperties { @@ -270,38 +327,41 @@ sealed interface TransitionState { internal abstract class BaseSceneTransitionLayoutState( initialScene: SceneKey, protected var stateLinks: List<StateLink>, -) : SceneTransitionLayoutState { - override var transitionState: TransitionState by - mutableStateOf(TransitionState.Idle(initialScene)) - protected set + // TODO(b/290930950): Remove this flag. + internal var enableInterruptions: Boolean, +) : SceneTransitionLayoutState { /** - * The current [transformationSpec] associated to [transitionState]. Accessing this value makes - * sense only if [transitionState] is a [TransitionState.Transition]. + * The current [TransitionState]. This list will either be: + * 1. A list with a single [TransitionState.Idle] element, when we are idle. + * 2. A list with one or more [TransitionState.Transition], when we are transitioning. */ - internal var transformationSpec: TransformationSpecImpl = TransformationSpec.Empty + @VisibleForTesting + internal val transitionStates: MutableList<TransitionState> = + SnapshotStateList<TransitionState>().apply { add(TransitionState.Idle(initialScene)) } - private var fromOverscrollSpec: OverscrollSpecImpl? = null - private var toOverscrollSpec: OverscrollSpecImpl? = null + override val transitionState: TransitionState + get() = transitionStates.last() - /** - * @return the overscroll [OverscrollSpecImpl] if it is defined for the current - * [transitionState] and we are currently over scrolling. - */ - internal val currentOverscrollSpec: OverscrollSpecImpl? + private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>() + + override val currentTransitions: List<TransitionState.Transition> get() { - val transition = currentTransition ?: return null - if (transition !is TransitionState.HasOverscrollProperties) return null - val progress = transition.progress - val bouncingScene = transition.bouncingScene - return when { - progress < 0f || bouncingScene == transition.fromScene -> fromOverscrollSpec - progress > 1f || bouncingScene == transition.toScene -> toOverscrollSpec - else -> null + if (transitionStates.last() is TransitionState.Idle) { + check(transitionStates.size == 1) + return emptyList() + } else { + @Suppress("UNCHECKED_CAST") + return transitionStates as List<TransitionState.Transition> } } - private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>() + /** + * The mapping of transitions that are finished, i.e. for which [finishTransition] was called, + * to their idle scene. + */ + @VisibleForTesting + internal val finishedTransitions = mutableMapOf<TransitionState.Transition, SceneKey>() /** Whether we can transition to the given [scene]. */ internal abstract fun canChangeScene(scene: SceneKey): Boolean @@ -324,7 +384,11 @@ internal abstract class BaseSceneTransitionLayoutState( return transition.isTransitioningBetween(scene, other) } - /** Start a new [transition], instantly interrupting any ongoing transition if there was one. */ + /** + * Start a new [transition], instantly interrupting any ongoing transition if there was one. + * + * Important: you *must* call [finishTransition] once the transition is finished. + */ internal fun startTransition( transition: TransitionState.Transition, transitionKey: TransitionKey?, @@ -333,13 +397,81 @@ internal abstract class BaseSceneTransitionLayoutState( val fromScene = transition.fromScene val toScene = transition.toScene val orientation = (transition as? TransitionState.HasOverscrollProperties)?.orientation - transformationSpec = + + // Update the transition specs. + transition.transformationSpec = transitions.transitionSpec(fromScene, toScene, key = transitionKey).transformationSpec() - fromOverscrollSpec = orientation?.let { transitions.overscrollSpec(fromScene, it) } - toOverscrollSpec = orientation?.let { transitions.overscrollSpec(toScene, it) } + if (orientation != null) { + transition.updateOverscrollSpecs( + fromSpec = transitions.overscrollSpec(fromScene, orientation), + toSpec = transitions.overscrollSpec(toScene, orientation), + ) + } else { + transition.updateOverscrollSpecs(fromSpec = null, toSpec = null) + } + + // Handle transition links. cancelActiveTransitionLinks() setupTransitionLinks(transition) - transitionState = transition + + if (!enableInterruptions) { + // Set the current transition. + check(transitionStates.size == 1) + transitionStates[0] = transition + return + } + + when (val currentState = transitionStates.last()) { + is TransitionState.Idle -> { + // Replace [Idle] by [transition]. + check(transitionStates.size == 1) + transitionStates[0] = transition + } + is TransitionState.Transition -> { + // Force the current transition to finish to currentScene. + currentState.finish().invokeOnCompletion { + // Make sure [finishTransition] is called at the end of the transition. + finishTransition(currentState, currentState.currentScene) + } + + // Check that we don't have too many concurrent transitions. + if (transitionStates.size >= MAX_CONCURRENT_TRANSITIONS) { + Log.wtf( + TAG, + buildString { + appendLine("Potential leak detected in SceneTransitionLayoutState!") + appendLine( + " Some transition(s) never called STLState.finishTransition()." + ) + appendLine(" Transitions (size=${transitionStates.size}):") + transitionStates.fastForEach { state -> + val transition = state as TransitionState.Transition + val from = transition.fromScene + val to = transition.toScene + val indicator = + if (finishedTransitions.contains(transition)) "x" else " " + appendLine(" [$indicator] $from => $to ($transition)") + } + } + ) + + // Force finish all transitions. + while (currentTransitions.isNotEmpty()) { + val transition = transitionStates[0] as TransitionState.Transition + finishTransition(transition, transition.currentScene) + } + + // We finished all transitions, so we are now idle. We remove this state so that + // we end up only with the new transition after appending it. + check(transitionStates.size == 1) + check(transitionStates[0] is TransitionState.Idle) + transitionStates.clear() + } + + // Append the new transition. + transitionStates.add(transition) + } + } } private fun cancelActiveTransitionLinks() { @@ -379,13 +511,54 @@ internal abstract class BaseSceneTransitionLayoutState( * nothing if [transition] was interrupted since it was started. */ internal fun finishTransition(transition: TransitionState.Transition, idleScene: SceneKey) { - resolveActiveTransitionLinks(idleScene) - if (transitionState == transition) { - transitionState = TransitionState.Idle(idleScene) + val existingIdleScene = finishedTransitions[transition] + if (existingIdleScene != null) { + // This transition was already finished. + check(idleScene == existingIdleScene) { + "Transition $transition was finished multiple times with different " + + "idleScene ($existingIdleScene != $idleScene)" + } + return + } + + if (!transitionStates.contains(transition)) { + // This transition was already removed from transitionStates. + return + } + + check(transitionStates.fastAll { it is TransitionState.Transition }) + + // Mark this transition as finished and save the scene it is settling at. + finishedTransitions[transition] = idleScene + + // Finish all linked transitions. + finishActiveTransitionLinks(idleScene) + + // Keep a reference to the idle scene of the last removed transition, in case we remove all + // transitions and should settle to Idle. + var lastRemovedIdleScene: SceneKey? = null + + // Remove all first n finished transitions. + while (transitionStates.isNotEmpty()) { + val firstTransition = transitionStates[0] + if (!finishedTransitions.contains(firstTransition)) { + // Stop here. + break + } + + // Remove the transition from the list and from the set of finished transitions. + transitionStates.removeAt(0) + lastRemovedIdleScene = finishedTransitions.remove(firstTransition) + } + + // If all transitions are finished, we are idle. + if (transitionStates.isEmpty()) { + check(finishedTransitions.isEmpty()) + transitionStates.add(TransitionState.Idle(checkNotNull(lastRemovedIdleScene))) } } - private fun resolveActiveTransitionLinks(idleScene: SceneKey) { + private fun finishActiveTransitionLinks(idleScene: SceneKey) { val previousTransition = this.transitionState as? TransitionState.Transition ?: return for ((link, linkedTransition) in activeTransitionLinks) { if (previousTransition.fromScene == idleScene) { @@ -406,20 +579,39 @@ internal abstract class BaseSceneTransitionLayoutState( * Check if a transition is in progress. If the progress value is near 0 or 1, immediately snap * to the closest scene. * + * Important: Snapping to the closest scene will instantly finish *all* ongoing transitions, + * only the progress of the last transition will be checked. + * * @return true if snapped to the closest scene. */ internal fun snapToIdleIfClose(threshold: Float): Boolean { val transition = currentTransition ?: return false val progress = transition.progress + fun isProgressCloseTo(value: Float) = (progress - value).absoluteValue <= threshold + fun finishAllTransitions(lastTransitionIdleScene: SceneKey) { + // Force finish all transitions. + while (currentTransitions.isNotEmpty()) { + val transition = transitionStates[0] as TransitionState.Transition + val idleScene = + if (transitionStates.size == 1) { + lastTransitionIdleScene + } else { + transition.currentScene + } + + finishTransition(transition, idleScene) + } + } + return when { isProgressCloseTo(0f) -> { - finishTransition(transition, transition.fromScene) + finishAllTransitions(transition.fromScene) true } isProgressCloseTo(1f) -> { - finishTransition(transition, transition.toScene) + finishAllTransitions(transition.toScene) true } else -> false @@ -437,7 +629,8 @@ internal class HoistedSceneTransitionLayoutState( private var changeScene: (SceneKey) -> Unit, private var canChangeScene: (SceneKey) -> Boolean, stateLinks: List<StateLink> = emptyList(), -) : BaseSceneTransitionLayoutState(initialScene, stateLinks) { + enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, +) : BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) { private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED) override fun canChangeScene(scene: SceneKey): Boolean = canChangeScene.invoke(scene) @@ -451,12 +644,14 @@ internal class HoistedSceneTransitionLayoutState( canChangeScene: (SceneKey) -> Boolean, transitions: SceneTransitions, stateLinks: List<StateLink>, + enableInterruptions: Boolean, ) { SideEffect { this.changeScene = onChangeScene this.canChangeScene = canChangeScene this.transitions = transitions this.stateLinks = stateLinks + this.enableInterruptions = enableInterruptions targetSceneChannel.trySend(currentScene) } @@ -482,7 +677,10 @@ internal class MutableSceneTransitionLayoutStateImpl( override var transitions: SceneTransitions, private val canChangeScene: (SceneKey) -> Boolean = { true }, stateLinks: List<StateLink> = emptyList(), -) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene, stateLinks) { + enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED, +) : + MutableSceneTransitionLayoutState, + BaseSceneTransitionLayoutState(initialScene, stateLinks, enableInterruptions) { override fun setTargetScene( targetScene: SceneKey, coroutineScope: CoroutineScope, @@ -501,3 +699,15 @@ internal class MutableSceneTransitionLayoutStateImpl( setTargetScene(scene, coroutineScope = this) } } + +private const val TAG = "SceneTransitionLayoutState" + +/** Whether support for interruptions in enabled by default. */ +internal const val DEFAULT_INTERRUPTIONS_ENABLED = true + +/** + * The max number of concurrent transitions. If the number of transitions goes past this number, + * this probably means that there is a leak and we will Log.wtf before clearing the list of + * transitions. + */ +private const val MAX_CONCURRENT_TRANSITIONS = 100 diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt index 228d19f09cff..b7abb33c1242 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt @@ -19,15 +19,9 @@ package com.android.compose.animation.scene import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntSize -internal class UserActionDistanceScopeImpl( +internal class ElementStateScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, -) : UserActionDistanceScope { - override val density: Float - get() = layoutImpl.density.density - - override val fontScale: Float - get() = layoutImpl.density.fontScale - +) : ElementStateScope { override fun ElementKey.targetSize(scene: SceneKey): IntSize? { return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf { it != Element.SizeUnspecified @@ -44,3 +38,13 @@ internal class UserActionDistanceScopeImpl( return layoutImpl.scenes[this]?.targetSize.takeIf { it != IntSize.Zero } } } + +internal class UserActionDistanceScopeImpl( + private val layoutImpl: SceneTransitionLayoutImpl, +) : UserActionDistanceScope, ElementStateScope by layoutImpl.elementStateScope { + override val density: Float + get() = layoutImpl.density.density + + override val fontScale: Float + get() = layoutImpl.density.fontScale +} diff --git a/packages/SystemUI/compose/scene/tests/Android.bp b/packages/SystemUI/compose/scene/tests/Android.bp index 59cc63aa5eef..af1389680bd2 100644 --- a/packages/SystemUI/compose/scene/tests/Android.bp +++ b/packages/SystemUI/compose/scene/tests/Android.bp @@ -26,7 +26,6 @@ android_test { name: "PlatformComposeSceneTransitionLayoutTests", manifest: "AndroidManifest.xml", test_suites: ["device-tests"], - sdk_version: "current", certificate: "platform", srcs: [ diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 1e9a7e2bb667..2ed51eb9a280 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -984,14 +984,14 @@ class DraggableHandlerTest { val scene = layoutState.transitionState.currentScene // We should have overscroll spec for scene C assertThat(layoutState.transitions.overscrollSpec(scene, Orientation.Vertical)).isNotNull() - assertThat(layoutState.currentOverscrollSpec).isNull() + assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNull() val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) // We scrolled down, under scene C there is nothing, so we can use the overscroll spec - assertThat(layoutState.currentOverscrollSpec).isNotNull() - assertThat(layoutState.currentOverscrollSpec?.scene).isEqualTo(SceneC) + assertThat(layoutState.currentTransition?.currentOverscrollSpec).isNotNull() + assertThat(layoutState.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneC) val transition = layoutState.currentTransition assertThat(transition).isNotNull() assertThat(transition!!.progress).isEqualTo(-0.1f) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 597da9e82a1f..2453e251b5a4 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -595,7 +595,7 @@ class ElementTest { } assertThat(state.currentTransition).isNull() - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() // Swipe by half of verticalSwipeDistance. rule.onRoot().performTouchInput { @@ -643,7 +643,7 @@ class ElementTest { // Scroll 150% (Scene B overscroll by 50%) assertThat(transition.progress).isEqualTo(1.5f) - assertThat(state.currentOverscrollSpec).isNotNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f) // animatedFloat cannot overflow (canOverflow = false) assertThat(animatedFloat).isEqualTo(100f) @@ -655,7 +655,7 @@ class ElementTest { // Scroll 250% (Scene B overscroll by 150%) assertThat(transition.progress).isEqualTo(2.5f) - assertThat(state.currentOverscrollSpec).isNotNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) assertThat(animatedFloat).isEqualTo(100f) } @@ -707,7 +707,7 @@ class ElementTest { } assertThat(state.currentTransition).isNull() - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true) fooElement.assertTopPositionInRootIsEqualTo(0.dp) @@ -720,7 +720,7 @@ class ElementTest { } val transition = state.currentTransition - assertThat(state.currentOverscrollSpec).isNotNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() assertThat(transition).isNotNull() assertThat(transition!!.progress).isEqualTo(-0.5f) fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f) @@ -732,7 +732,7 @@ class ElementTest { // Scroll 150% (Scene B overscroll by 50%) assertThat(transition.progress).isEqualTo(-1.5f) - assertThat(state.currentOverscrollSpec).isNotNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) } @@ -771,7 +771,7 @@ class ElementTest { // Scroll 150% (100% scroll + 50% overscroll) assertThat(transition!!.progress).isEqualTo(1.5f) - assertThat(state.currentOverscrollSpec).isNotNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f) assertThat(animatedFloat).isEqualTo(100f) @@ -782,7 +782,7 @@ class ElementTest { // Scroll 250% (100% scroll + 150% overscroll) assertThat(transition.progress).isEqualTo(2.5f) - assertThat(state.currentOverscrollSpec).isNotNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f) assertThat(animatedFloat).isEqualTo(100f) } @@ -828,7 +828,7 @@ class ElementTest { // Scroll 150% (100% scroll + 50% overscroll) assertThat(transition.progress).isEqualTo(1.5f) - assertThat(state.currentOverscrollSpec).isNotNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * (transition.progress - 1f)) assertThat(animatedFloat).isEqualTo(100f) @@ -840,7 +840,7 @@ class ElementTest { rule.waitUntil(timeoutMillis = 10_000) { transition.progress < 1f } assertThat(transition.progress).isLessThan(1f) - assertThat(state.currentOverscrollSpec).isNotNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() assertThat(transition.bouncingScene).isEqualTo(transition.toScene) assertThat(animatedFloat).isEqualTo(100f) } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index 9baabc3cfb57..93e94f8f95a2 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -16,6 +16,7 @@ package com.android.compose.animation.scene +import android.util.Log import androidx.compose.foundation.gestures.Orientation import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.test.junit4.createComposeRule @@ -28,9 +29,12 @@ import com.android.compose.animation.scene.transition.link.StateLink import com.android.compose.test.runMonotonicClockTest import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.job import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -271,11 +275,21 @@ class SceneTransitionLayoutStateTest { } @Test - fun linkedTransition_startsLinkButLinkedStateIsTakenOver() { + fun linkedTransition_startsLinkButLinkedStateIsTakenOver() = runTest { val (parentState, childState) = setupLinkedStates() - val childTransition = transition(SceneA, SceneB) - val parentTransition = transition(SceneC, SceneA) + val childTransition = + transition( + SceneA, + SceneB, + onFinish = { launch { /* Do nothing. */} }, + ) + val parentTransition = + transition( + SceneC, + SceneA, + onFinish = { launch { /* Do nothing. */} }, + ) childState.startTransition(childTransition, null) parentState.startTransition(parentTransition, null) @@ -303,7 +317,7 @@ class SceneTransitionLayoutStateTest { // Default transition from A to B. assertThat(state.setTargetScene(SceneB, coroutineScope = this)).isNotNull() - assertThat(state.transformationSpec.transformations).hasSize(1) + assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1) // Go back to A. state.setTargetScene(SceneA, coroutineScope = this) @@ -320,14 +334,14 @@ class SceneTransitionLayoutStateTest { ) ) .isNotNull() - assertThat(state.transformationSpec.transformations).hasSize(2) + assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2) } @Test fun snapToIdleIfClose_snapToStart() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty) state.startTransition( - transition(from = SceneA, to = TestScenes.SceneB, progress = { 0.2f }), + transition(from = SceneA, to = SceneB, progress = { 0.2f }), transitionKey = null ) assertThat(state.isTransitioning()).isTrue() @@ -346,7 +360,7 @@ class SceneTransitionLayoutStateTest { fun snapToIdleIfClose_snapToEnd() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty) state.startTransition( - transition(from = SceneA, to = TestScenes.SceneB, progress = { 0.8f }), + transition(from = SceneA, to = SceneB, progress = { 0.8f }), transitionKey = null ) assertThat(state.isTransitioning()).isTrue() @@ -358,7 +372,35 @@ class SceneTransitionLayoutStateTest { // Go to the final scene if it is close to 1. assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue() assertThat(state.isTransitioning()).isFalse() - assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneB)) + assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneB)) + } + + @Test + fun snapToIdleIfClose_multipleTransitions() = runMonotonicClockTest { + val state = MutableSceneTransitionLayoutStateImpl(SceneA, SceneTransitions.Empty) + + val aToB = + transition( + from = SceneA, + to = SceneB, + progress = { 0.5f }, + onFinish = { launch { /* do nothing */} }, + ) + state.startTransition(aToB, transitionKey = null) + assertThat(state.currentTransitions).containsExactly(aToB).inOrder() + + val bToC = transition(from = SceneB, to = SceneC, progress = { 0.8f }) + state.startTransition(bToC, transitionKey = null) + assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder() + + // Ignore the request if the progress is not close to 0 or 1, using the threshold. + assertThat(state.snapToIdleIfClose(threshold = 0.1f)).isFalse() + assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder() + + // Go to the final scene if it is close to 1. + assertThat(state.snapToIdleIfClose(threshold = 0.2f)).isTrue() + assertThat(state.transitionState).isEqualTo(TransitionState.Idle(SceneC)) + assertThat(state.currentTransitions).isEmpty() } @Test @@ -435,23 +477,23 @@ class SceneTransitionLayoutStateTest { overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) } } ) - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() // overscroll for SceneA is NOT defined progress.value = -0.1f - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() // scroll from SceneA to SceneB progress.value = 0.5f - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() progress.value = 1f - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() // overscroll for SceneB is defined progress.value = 1.1f - assertThat(state.currentOverscrollSpec).isNotNull() - assertThat(state.currentOverscrollSpec?.scene).isEqualTo(SceneB) + assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() + assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneB) } @Test @@ -465,23 +507,23 @@ class SceneTransitionLayoutStateTest { overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) } } ) - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() // overscroll for SceneA is defined progress.value = -0.1f - assertThat(state.currentOverscrollSpec).isNotNull() - assertThat(state.currentOverscrollSpec?.scene).isEqualTo(SceneA) + assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() + assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneA) // scroll from SceneA to SceneB progress.value = 0.5f - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() progress.value = 1f - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() // overscroll for SceneB is NOT defined progress.value = 1.1f - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() } @Test @@ -492,21 +534,99 @@ class SceneTransitionLayoutStateTest { progress = { progress.value }, sceneTransitions = transitions {} ) - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() // overscroll for SceneA is NOT defined progress.value = -0.1f - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() // scroll from SceneA to SceneB progress.value = 0.5f - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() progress.value = 1f - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() // overscroll for SceneB is NOT defined progress.value = 1.1f - assertThat(state.currentOverscrollSpec).isNull() + assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + } + + @Test + fun multipleTransitions() = runTest { + val finishingTransitions = mutableSetOf<TransitionState.Transition>() + fun onFinish(transition: TransitionState.Transition): Job { + // Instead of letting the transition finish, we put the transition in the + // finishingTransitions set so that we can verify that finish() is called when expected + // and then we call state STLState.finishTransition() ourselves. + finishingTransitions.add(transition) + + return backgroundScope.launch { + // Try to acquire a locked mutex so that this code never completes. + Mutex(locked = true).withLock {} + } + } + + val state = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions) + val aToB = transition(SceneA, SceneB, onFinish = ::onFinish) + val bToC = transition(SceneB, SceneC, onFinish = ::onFinish) + val cToA = transition(SceneC, SceneA, onFinish = ::onFinish) + + // Starting state. + assertThat(finishingTransitions).isEmpty() + assertThat(state.currentTransitions).isEmpty() + + // A => B. + state.startTransition(aToB, transitionKey = null) + assertThat(finishingTransitions).isEmpty() + assertThat(state.finishedTransitions).isEmpty() + assertThat(state.currentTransitions).containsExactly(aToB).inOrder() + + // B => C. This should automatically call finish() on aToB. + state.startTransition(bToC, transitionKey = null) + assertThat(finishingTransitions).containsExactly(aToB) + assertThat(state.finishedTransitions).isEmpty() + assertThat(state.currentTransitions).containsExactly(aToB, bToC).inOrder() + + // C => A. This should automatically call finish() on bToC. + state.startTransition(cToA, transitionKey = null) + assertThat(finishingTransitions).containsExactly(aToB, bToC) + assertThat(state.finishedTransitions).isEmpty() + assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder() + + // Mark bToC as finished. The list of current transitions does not change because aToB is + // still not marked as finished. + state.finishTransition(bToC, idleScene = bToC.currentScene) + assertThat(state.finishedTransitions).containsExactly(bToC, bToC.currentScene) + assertThat(state.currentTransitions).containsExactly(aToB, bToC, cToA).inOrder() + + // Mark aToB as finished. This will remove both aToB and bToC from the list of transitions. + state.finishTransition(aToB, idleScene = aToB.currentScene) + assertThat(state.finishedTransitions).isEmpty() + assertThat(state.currentTransitions).containsExactly(cToA).inOrder() + } + + @Test + fun tooManyTransitionsLogsWtfAndClearsTransitions() = runTest { + val state = MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions) + + fun startTransition() { + val transition = transition(SceneA, SceneB, onFinish = { launch { /* do nothing */} }) + state.startTransition(transition, transitionKey = null) + } + + var hasLoggedWtf = false + val originalHandler = Log.setWtfHandler { _, _, _ -> hasLoggedWtf = true } + try { + repeat(100) { startTransition() } + assertThat(hasLoggedWtf).isFalse() + assertThat(state.currentTransitions).hasSize(100) + + startTransition() + assertThat(hasLoggedWtf).isTrue() + assertThat(state.currentTransitions).hasSize(1) + } finally { + Log.setWtfHandler(originalHandler) + } } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index efaea71f8d2c..723a1825f205 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -299,6 +299,11 @@ class SceneTransitionLayoutTest { .isWithin(DpOffsetSubject.DefaultTolerance) .of(DpOffset(expectedOffset, expectedOffset)) + // Wait for the transition to C to finish. + rule.mainClock.advanceTimeBy(TestTransitionDuration) + assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) + assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + // Go back to scene A. This should happen instantly (once the animation started, i.e. after // 2 frames) given that we use a snap() animation spec. currentScene = TestScenes.SceneA diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index 99372a5d084b..f034c184b794 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -547,12 +547,12 @@ class SwipeToSceneTest { } assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue() - assertThat(state.transformationSpec.transformations).hasSize(1) + assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1) // Move the pointer up to swipe to scene B using the new transition. rule.onRoot().performTouchInput { moveBy(Offset(0f, -1.dp.toPx()), delayMillis = 1_000) } assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue() - assertThat(state.transformationSpec.transformations).hasSize(2) + assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2) } @Test diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt index a32fe2273804..767057b585b8 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/Transition.kt @@ -29,6 +29,7 @@ fun transition( isUpOrLeft: Boolean = false, bouncingScene: SceneKey? = null, orientation: Orientation = Orientation.Horizontal, + onFinish: ((TransitionState.Transition) -> Job)? = null, ): TransitionState.Transition { return object : TransitionState.Transition(from, to), TransitionState.HasOverscrollProperties { override val currentScene: SceneKey = from @@ -46,7 +47,13 @@ fun transition( } override fun finish(): Job { - error("finish() is not supported in test transitions") + val onFinish = + onFinish + ?: error( + "onFinish() must be provided if finish() is called on test transitions" + ) + + return onFinish(this) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java index 343280de17b8..289896e01a9d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -444,6 +444,17 @@ public class AuthControllerTest extends SysuiTestCase { AdditionalMatchers.aryEq(credentialAttestation)); } + @Test + public void testSendsReasonContentViewMoreOptions_whenButtonPressed() throws Exception { + showDialog(new int[]{1} /* sensorIds */, false /* credentialAllowed */); + mAuthController.onDismissed(AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS, + null, /* credentialAttestation */ + mAuthController.mCurrentDialog.getRequestId()); + verify(mReceiver).onDialogDismissed( + eq(BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS), + eq(null) /* credentialAttestation */); + } + // Statusbar tests @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index 707777b9f728..b0d03b15d310 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -71,34 +71,6 @@ class BouncerInteractorTest : SysuiTestCase() { } @Test - fun pinAuthMethod() = - testScope.runTest { - val message by collectLastValue(underTest.message) - - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin - ) - runCurrent() - underTest.clearMessage() - assertThat(message).isNull() - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) - - // Wrong input. - assertThat(underTest.authenticate(listOf(9, 8, 7))) - .isEqualTo(AuthenticationResult.FAILED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PIN) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) - - // Correct input. - assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)) - .isEqualTo(AuthenticationResult.SUCCEEDED) - } - - @Test fun pinAuthMethod_sim_skipsAuthentication() = testScope.runTest { kosmos.fakeAuthenticationRepository.setAuthenticationMethod( @@ -146,8 +118,6 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun pinAuthMethod_tryAutoConfirm_withoutAutoConfirmPin() = testScope.runTest { - val message by collectLastValue(underTest.message) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin ) @@ -156,7 +126,6 @@ class BouncerInteractorTest : SysuiTestCase() { // Incomplete input. assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true)) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isNull() // Correct input. assertThat( @@ -166,28 +135,19 @@ class BouncerInteractorTest : SysuiTestCase() { ) ) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isNull() } @Test fun passwordAuthMethod() = testScope.runTest { - val message by collectLastValue(underTest.message) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) runCurrent() - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) - // Wrong input. assertThat(underTest.authenticate("alohamora".toList())) .isEqualTo(AuthenticationResult.FAILED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) // Too short input. assertThat( @@ -201,7 +161,6 @@ class BouncerInteractorTest : SysuiTestCase() { ) ) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD) // Correct input. assertThat(underTest.authenticate("password".toList())) @@ -211,13 +170,10 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun patternAuthMethod() = testScope.runTest { - val message by collectLastValue(underTest.message) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pattern ) runCurrent() - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) // Wrong input. val wrongPattern = @@ -231,10 +187,6 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat(wrongPattern.size) .isAtLeast(kosmos.fakeAuthenticationRepository.minPatternLength) assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) // Too short input. val tooShortPattern = @@ -244,10 +196,6 @@ class BouncerInteractorTest : SysuiTestCase() { ) assertThat(underTest.authenticate(tooShortPattern)) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) // Correct input. assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN)) @@ -258,7 +206,6 @@ class BouncerInteractorTest : SysuiTestCase() { fun lockoutStarted() = testScope.runTest { val lockoutStartedEvents by collectValues(underTest.onLockoutStarted) - val message by collectLastValue(underTest.message) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin @@ -272,17 +219,14 @@ class BouncerInteractorTest : SysuiTestCase() { .isEqualTo(AuthenticationResult.FAILED) if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { assertThat(lockoutStartedEvents).isEmpty() - assertThat(message).isNotEmpty() } } assertThat(authenticationInteractor.lockoutEndTimestamp).isNotNull() assertThat(lockoutStartedEvents.size).isEqualTo(1) - assertThat(message).isNull() // Advance the time to finish the lockout: advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds) assertThat(authenticationInteractor.lockoutEndTimestamp).isNull() - assertThat(message).isNull() assertThat(lockoutStartedEvents.size).isEqualTo(1) // Trigger lockout again: diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt index 701b7039a1ed..c878e0b4757d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.bouncer.domain.interactor import android.content.pm.UserInfo -import android.os.Handler import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -28,27 +27,25 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FaceSensorInfo -import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository +import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository +import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.bouncer.shared.model.BouncerMessageModel -import com.android.systemui.bouncer.ui.BouncerView -import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor import com.android.systemui.flags.SystemPropertiesHelper -import com.android.systemui.keyguard.DismissCallbackRegistry -import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.data.repository.FakeTrustRepository +import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeTrustRepository import com.android.systemui.keyguard.shared.model.AuthenticationFlags +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown import com.android.systemui.res.R.string.kg_trust_agent_disabled -import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.KotlinArgumentCaptor import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -61,7 +58,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mock -import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -70,34 +66,22 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) class BouncerMessageInteractorTest : SysuiTestCase() { - + private val kosmos = testKosmos() private val countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java) private val repository = BouncerMessageRepositoryImpl() - private val userRepository = FakeUserRepository() - private val fakeTrustRepository = FakeTrustRepository() - private val fakeFacePropertyRepository = FakeFacePropertyRepository() - private val bouncerRepository = FakeKeyguardBouncerRepository() - private val fakeDeviceEntryFingerprintAuthRepository = - FakeDeviceEntryFingerprintAuthRepository() - private val fakeDeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository() - private val biometricSettingsRepository: FakeBiometricSettingsRepository = - FakeBiometricSettingsRepository() + private val biometricSettingsRepository = kosmos.fakeBiometricSettingsRepository + private val testScope = kosmos.testScope @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor @Mock private lateinit var securityModel: KeyguardSecurityModel @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil @Mock private lateinit var systemPropertiesHelper: SystemPropertiesHelper - @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor - private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor - private lateinit var testScope: TestScope private lateinit var underTest: BouncerMessageInteractor @Before fun setUp() { MockitoAnnotations.initMocks(this) - userRepository.setUserInfos(listOf(PRIMARY_USER)) - testScope = TestScope() + kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER)) allowTestableLooperAsMainThread() whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) @@ -105,44 +89,28 @@ class BouncerMessageInteractorTest : SysuiTestCase() { } suspend fun TestScope.init() { - userRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES) - primaryBouncerInteractor = - PrimaryBouncerInteractor( - bouncerRepository, - mock(BouncerView::class.java), - mock(Handler::class.java), - mock(KeyguardStateController::class.java), - mock(KeyguardSecurityModel::class.java), - mock(PrimaryBouncerCallbackInteractor::class.java), - mock(FalsingCollector::class.java), - mock(DismissCallbackRegistry::class.java), - context, - keyguardUpdateMonitor, - fakeTrustRepository, - testScope.backgroundScope, - mSelectedUserInteractor, - mock(DeviceEntryFaceAuthInteractor::class.java), - ) underTest = BouncerMessageInteractor( repository = repository, - userRepository = userRepository, + userRepository = kosmos.fakeUserRepository, countDownTimerUtil = countDownTimerUtil, updateMonitor = updateMonitor, biometricSettingsRepository = biometricSettingsRepository, - applicationScope = this.backgroundScope, - trustRepository = fakeTrustRepository, + applicationScope = testScope.backgroundScope, + trustRepository = kosmos.fakeTrustRepository, systemPropertiesHelper = systemPropertiesHelper, - primaryBouncerInteractor = primaryBouncerInteractor, - facePropertyRepository = fakeFacePropertyRepository, - deviceEntryFingerprintAuthRepository = fakeDeviceEntryFingerprintAuthRepository, - faceAuthRepository = fakeDeviceEntryFaceAuthRepository, + primaryBouncerInteractor = kosmos.primaryBouncerInteractor, + facePropertyRepository = kosmos.fakeFacePropertyRepository, + deviceEntryFingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor, + faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository, securityModel = securityModel ) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) - fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) - bouncerRepository.setPrimaryShow(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeKeyguardBouncerRepository.setPrimaryShow(true) runCurrent() } @@ -268,7 +236,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { init() val lockoutMessage by collectLastValue(underTest.bouncerMessage) - fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) runCurrent() assertThat(primaryResMessage(lockoutMessage)) @@ -276,7 +244,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { assertThat(secondaryResMessage(lockoutMessage)) .isEqualTo("Can’t unlock with face. Too many attempts.") - fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) runCurrent() assertThat(primaryResMessage(lockoutMessage)) @@ -289,15 +257,17 @@ class BouncerMessageInteractorTest : SysuiTestCase() { testScope.runTest { init() val lockoutMessage by collectLastValue(underTest.bouncerMessage) - fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG)) - fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + kosmos.fakeFacePropertyRepository.setSensorInfo( + FaceSensorInfo(1, SensorStrength.STRONG) + ) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) runCurrent() assertThat(primaryResMessage(lockoutMessage)).isEqualTo("Enter PIN") assertThat(secondaryResMessage(lockoutMessage)) .isEqualTo("PIN is required after too many attempts") - fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) runCurrent() assertThat(primaryResMessage(lockoutMessage)) @@ -311,14 +281,14 @@ class BouncerMessageInteractorTest : SysuiTestCase() { init() val lockedOutMessage by collectLastValue(underTest.bouncerMessage) - fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) runCurrent() assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN") assertThat(secondaryResMessage(lockedOutMessage)) .isEqualTo("PIN is required after too many attempts") - fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) runCurrent() assertThat(primaryResMessage(lockedOutMessage)) @@ -327,6 +297,19 @@ class BouncerMessageInteractorTest : SysuiTestCase() { } @Test + fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() = + testScope.runTest { + init() + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + kosmos.fakeFingerprintPropertyRepository.supportsUdfps() + val lockedOutMessage by collectLastValue(underTest.bouncerMessage) + + runCurrent() + + assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN") + } + + @Test fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() = testScope.runTest { init() @@ -344,9 +327,10 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() = testScope.runTest { init() - fakeTrustRepository.setTrustUsuallyManaged(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + runCurrent() val defaultMessage = Pair("Enter PIN", null) @@ -377,12 +361,13 @@ class BouncerMessageInteractorTest : SysuiTestCase() { testScope.runTest { init() - userRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + runCurrent() - fakeTrustRepository.setCurrentUserTrustManaged(true) - fakeTrustRepository.setTrustUsuallyManaged(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(true) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) val defaultMessage = Pair("Enter PIN", null) @@ -415,8 +400,8 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() = testScope.runTest { init() - userRepository.setSelectedUserInfo(PRIMARY_USER) - fakeTrustRepository.setTrustUsuallyManaged(false) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) @@ -453,12 +438,13 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() = testScope.runTest { init() - userRepository.setSelectedUserInfo(PRIMARY_USER) - fakeTrustRepository.setCurrentUserTrustManaged(false) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + runCurrent() verifyMessagesForAuthFlag( LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to @@ -466,6 +452,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { ) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false) + runCurrent() verifyMessagesForAuthFlag( LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index d30e33332926..c9fa671ad34f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -48,6 +48,7 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, + onIntentionalUserInput = {}, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt new file mode 100644 index 000000000000..16ec9aa897fb --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.bouncer.ui.viewmodel + +import android.content.pm.UserInfo +import android.hardware.biometrics.BiometricFaceConstants +import android.hardware.fingerprint.FingerprintManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin +import com.android.systemui.biometrics.data.repository.FaceSensorInfo +import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository +import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.shared.flag.fakeComposeBouncerFlags +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus +import com.android.systemui.flags.fakeSystemPropertiesHelper +import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeTrustRepository +import com.android.systemui.keyguard.shared.model.AuthenticationFlags +import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.currentTime +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 BouncerMessageViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val authenticationInteractor by lazy { kosmos.authenticationInteractor } + private val bouncerInteractor by lazy { kosmos.bouncerInteractor } + private lateinit var underTest: BouncerMessageViewModel + + @Before + fun setUp() { + kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER)) + kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true + underTest = kosmos.bouncerMessageViewModel + overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable") + kosmos.fakeSystemPropertiesHelper.set( + DeviceEntryInteractor.SYS_BOOT_REASON_PROP, + "not mainline reboot" + ) + } + + @Test + fun message_defaultMessage_basedOnAuthMethod() = + testScope.runTest { + val message by collectLastValue(underTest.message) + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + runCurrent() + + assertThat(message!!.text).isEqualTo("Unlock with PIN or fingerprint") + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern) + runCurrent() + assertThat(message!!.text).isEqualTo("Unlock with pattern or fingerprint") + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Password + ) + runCurrent() + assertThat(message!!.text).isEqualTo("Unlock with password or fingerprint") + } + + @Test + fun message() = + testScope.runTest { + val message by collectLastValue(underTest.message) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + assertThat(message?.isUpdateAnimated).isTrue() + + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { + bouncerInteractor.authenticate(WRONG_PIN) + } + assertThat(message?.isUpdateAnimated).isFalse() + + val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0 + advanceTimeBy(lockoutEndMs - testScope.currentTime) + assertThat(message?.isUpdateAnimated).isTrue() + } + + @Test + fun lockoutMessage() = + testScope.runTest { + val message by collectLastValue(underTest.message) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull() + runCurrent() + + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times -> + bouncerInteractor.authenticate(WRONG_PIN) + runCurrent() + if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { + assertThat(message?.text).isEqualTo("Wrong PIN. Try again.") + assertThat(message?.isUpdateAnimated).isTrue() + } + } + val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS + assertTryAgainMessage(message?.text, lockoutSeconds) + assertThat(message?.isUpdateAnimated).isFalse() + + repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time -> + advanceTimeBy(1.seconds) + val remainingSeconds = lockoutSeconds - time - 1 + if (remainingSeconds > 0) { + assertTryAgainMessage(message?.text, remainingSeconds) + } + } + assertThat(message?.text).isEqualTo("Enter PIN") + assertThat(message?.isUpdateAnimated).isTrue() + } + + @Test + fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenTrustAgentIsEnabled() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) + runCurrent() + + val defaultMessage = Pair("Enter PIN", null) + + verifyMessagesForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to defaultMessage, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair("Enter PIN", "PIN is required after device restarts"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + Pair("Enter PIN", "Added security required. PIN not used for a while."), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + Pair("Enter PIN", "For added security, device was locked by work policy"), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to + Pair("Enter PIN", "Trust agent is unavailable"), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + Pair("Enter PIN", "Trust agent is unavailable"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + Pair("Enter PIN", "PIN is required after lockdown"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + Pair("Enter PIN", "PIN required for additional security"), + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + Pair( + "Enter PIN", + "Added security required. Device wasn’t unlocked for a while." + ), + ) + } + + @Test + fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenFingerprintIsAvailable() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + runCurrent() + + verifyMessagesForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to + Pair("Unlock with PIN or fingerprint", null), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to + Pair("Unlock with PIN or fingerprint", null), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + Pair("Unlock with PIN or fingerprint", null), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair("Enter PIN", "PIN is required after device restarts"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + Pair("Enter PIN", "Added security required. PIN not used for a while."), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + Pair("Enter PIN", "For added security, device was locked by work policy"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + Pair("Enter PIN", "PIN is required after lockdown"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + Pair("Enter PIN", "PIN required for additional security"), + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + Pair( + "Unlock with PIN or fingerprint", + "Added security required. Device wasn’t unlocked for a while." + ), + ) + } + + @Test + fun onFingerprintLockout_messageUpdated() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + val lockedOutMessage by collectLastValue(underTest.message) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(lockedOutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockedOutMessage?.secondaryText) + .isEqualTo("PIN is required after too many attempts") + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + runCurrent() + + assertThat(lockedOutMessage?.text).isEqualTo("Unlock with PIN or fingerprint") + assertThat(lockedOutMessage?.secondaryText.isNullOrBlank()).isTrue() + } + + @Test + fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeFingerprintPropertyRepository.supportsUdfps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + val message by collectLastValue(underTest.message) + + runCurrent() + + assertThat(message?.text).isEqualTo("Enter PIN") + } + + @Test + fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeSystemPropertiesHelper.set("sys.boot.reason.last", "reboot,mainline_update") + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + runCurrent() + + verifyMessagesForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair("Enter PIN", "Device updated. Enter PIN to continue.") + ) + } + + @Test + fun onFaceLockout_whenItIsClass3_shouldProvideRelevantMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + val lockoutMessage by collectLastValue(underTest.message) + kosmos.fakeFacePropertyRepository.setSensorInfo( + FaceSensorInfo(1, SensorStrength.STRONG) + ) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText) + .isEqualTo("PIN is required after too many attempts") + + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue() + } + + @Test + fun onFaceLockout_whenItIsNotStrong_shouldProvideRelevantMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + val lockoutMessage by collectLastValue(underTest.message) + kosmos.fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.WEAK)) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText) + .isEqualTo("Can’t unlock with face. Too many attempts.") + + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue() + } + + @Test + fun setFingerprintMessage_propagateValue() = + testScope.runTest { + val bouncerMessage by collectLastValue(underTest.message) + + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + runCurrent() + + kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus( + HelpFingerprintAuthenticationStatus(1, "some helpful message") + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Unlock with PIN or fingerprint") + assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message") + + kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus( + FailFingerprintAuthenticationStatus + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Fingerprint not recognized") + assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN") + + kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "locked out" + ) + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText) + .isEqualTo("PIN is required after too many attempts") + } + + @Test + fun setFaceMessage_propagateValue() = + testScope.runTest { + val bouncerMessage by collectLastValue(underTest.message) + + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true) + runCurrent() + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus(1, "some helpful message") + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message") + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + ErrorFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ERROR_TIMEOUT, + "Try again" + ) + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again") + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + FailedFaceAuthenticationStatus() + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Face not recognized") + assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN") + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + ErrorFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ERROR_LOCKOUT, + "locked out" + ) + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText) + .isEqualTo("Can’t unlock with face. Too many attempts.") + } + + private fun TestScope.verifyMessagesForAuthFlags( + vararg authFlagToMessagePair: Pair<Int, Pair<String, String?>> + ) { + val actualMessage by collectLastValue(underTest.message) + + authFlagToMessagePair.forEach { (flag, expectedMessagePair) -> + kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( + AuthenticationFlags(userId = PRIMARY_USER_ID, flag = flag) + ) + runCurrent() + + assertThat(actualMessage?.text).isEqualTo(expectedMessagePair.first) + + if (expectedMessagePair.second == null) { + assertThat(actualMessage?.secondaryText.isNullOrBlank()).isTrue() + } else { + assertThat(actualMessage?.secondaryText).isEqualTo(expectedMessagePair.second) + } + } + } + + private fun assertTryAgainMessage( + message: String?, + time: Int, + ) { + assertThat(message).contains("Try again in $time second") + } + + companion object { + private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } + private const val PRIMARY_USER_ID = 0 + private val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index 73db1757c06a..3afca96e07a0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -37,7 +37,6 @@ import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest @@ -142,54 +141,6 @@ class BouncerViewModelTest : SysuiTestCase() { } @Test - fun message() = - testScope.runTest { - val message by collectLastValue(underTest.message) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) - assertThat(message?.isUpdateAnimated).isTrue() - - repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { - bouncerInteractor.authenticate(WRONG_PIN) - } - assertThat(message?.isUpdateAnimated).isFalse() - - val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0 - advanceTimeBy(lockoutEndMs - testScope.currentTime) - assertThat(message?.isUpdateAnimated).isTrue() - } - - @Test - fun lockoutMessage() = - testScope.runTest { - val authMethodViewModel by collectLastValue(underTest.authMethodViewModel) - val message by collectLastValue(underTest.message) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) - assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull() - assertThat(authMethodViewModel?.lockoutMessageId).isNotNull() - - repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times -> - bouncerInteractor.authenticate(WRONG_PIN) - if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { - assertThat(message?.text).isEqualTo(bouncerInteractor.message.value) - assertThat(message?.isUpdateAnimated).isTrue() - } - } - val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS - assertTryAgainMessage(message?.text, lockoutSeconds) - assertThat(message?.isUpdateAnimated).isFalse() - - repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time -> - advanceTimeBy(1.seconds) - val remainingSeconds = lockoutSeconds - time - 1 - if (remainingSeconds > 0) { - assertTryAgainMessage(message?.text, remainingSeconds) - } - } - assertThat(message?.text).isEmpty() - assertThat(message?.isUpdateAnimated).isTrue() - } - - @Test fun isInputEnabled() = testScope.runTest { val isInputEnabled by @@ -212,25 +163,6 @@ class BouncerViewModelTest : SysuiTestCase() { } @Test - fun dialogViewModel() = - testScope.runTest { - val authMethodViewModel by collectLastValue(underTest.authMethodViewModel) - val dialogViewModel by collectLastValue(underTest.dialogViewModel) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) - assertThat(authMethodViewModel?.lockoutMessageId).isNotNull() - - repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { - assertThat(dialogViewModel).isNull() - bouncerInteractor.authenticate(WRONG_PIN) - } - assertThat(dialogViewModel).isNotNull() - assertThat(dialogViewModel?.text).isNotEmpty() - - dialogViewModel?.onDismiss?.invoke() - assertThat(dialogViewModel).isNull() - } - - @Test fun isSideBySideSupported() = testScope.runTest { val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported) @@ -265,13 +197,6 @@ class BouncerViewModelTest : SysuiTestCase() { return listOf(None, Pin, Password, Pattern, Sim) } - private fun assertTryAgainMessage( - message: String?, - time: Int, - ) { - assertThat(message).isEqualTo("Try again in $time seconds.") - } - companion object { private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index df50eb64f8b6..71c578545647 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -66,7 +66,6 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { private val bouncerInteractor by lazy { kosmos.bouncerInteractor } private val selectedUserInteractor by lazy { kosmos.selectedUserInteractor } private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor } - private val bouncerViewModel by lazy { kosmos.bouncerViewModel } private val isInputEnabled = MutableStateFlow(true) private val underTest = @@ -76,6 +75,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { interactor = bouncerInteractor, inputMethodInteractor = inputMethodInteractor, selectedUserInteractor = selectedUserInteractor, + onIntentionalUserInput = {}, ) @Before @@ -88,11 +88,9 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { fun onShown() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) assertThat(password).isEmpty() assertThat(currentScene).isEqualTo(Scenes.Bouncer) assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password) @@ -101,16 +99,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onHidden_resetsPasswordInputAndMessage() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() underTest.onPasswordInputChanged("password") - assertThat(message?.text).isNotEqualTo(ENTER_YOUR_PASSWORD) assertThat(password).isNotEmpty() underTest.onHidden() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) assertThat(password).isEmpty() } @@ -118,13 +113,11 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { fun onPasswordInputChanged() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() underTest.onPasswordInputChanged("password") - assertThat(message?.text).isEmpty() assertThat(password).isEqualTo("password") assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -144,7 +137,6 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateKeyPressed_whenWrong() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() @@ -152,13 +144,11 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateKeyPressed() assertThat(password).isEmpty() - assertThat(message?.text).isEqualTo(WRONG_PASSWORD) } @Test fun onAuthenticateKeyPressed_whenEmpty() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password @@ -171,14 +161,12 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateKeyPressed() assertThat(password).isEmpty() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) } @Test fun onAuthenticateKeyPressed_correctAfterWrong() = testScope.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() @@ -186,12 +174,10 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { underTest.onPasswordInputChanged("wrong") underTest.onAuthenticateKeyPressed() assertThat(password).isEqualTo("") - assertThat(message?.text).isEqualTo(WRONG_PASSWORD) assertThat(authResult).isFalse() // Enter the correct password: underTest.onPasswordInputChanged("password") - assertThat(message?.text).isEmpty() underTest.onAuthenticateKeyPressed() @@ -331,10 +317,8 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) - val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer sceneInteractor.changeScene(toScene, "reason") - if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 91a056ddd685..51b73ee92df5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -63,6 +63,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), + onIntentionalUserInput = {}, ) } @@ -79,12 +80,10 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onShown() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN) assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(currentScene).isEqualTo(Scenes.Bouncer) @@ -95,14 +94,12 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onDragStart() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() underTest.onDragStart() - assertThat(message?.text).isEmpty() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(currentScene).isEqualTo(Scenes.Bouncer) @@ -148,7 +145,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onDragEnd_whenWrong() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() @@ -159,7 +155,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() - assertThat(message?.text).isEqualTo(WRONG_PATTERN) assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -302,7 +297,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragEnd_whenPatternTooShort() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val dialogViewModel by collectLastValue(bouncerViewModel.dialogViewModel) lockDeviceAndOpenPatternBouncer() @@ -325,7 +319,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { underTest.onDragEnd() - assertWithMessage("Attempt #$attempt").that(message?.text).isEqualTo(WRONG_PATTERN) assertWithMessage("Attempt #$attempt").that(dialogViewModel).isNull() } } @@ -334,7 +327,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onDragEnd_correctAfterWrong() = testScope.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() @@ -344,7 +336,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { underTest.onDragEnd() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() - assertThat(message?.text).isEqualTo(WRONG_PATTERN) assertThat(authResult).isFalse() // Enter the correct pattern: @@ -370,10 +361,8 @@ class PatternBouncerViewModelTest : SysuiTestCase() { private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) - val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer sceneInteractor.changeScene(toScene, "reason") - if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 7b75a3715415..564795429fa6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -56,7 +56,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { private val sceneInteractor by lazy { kosmos.sceneInteractor } private val authenticationInteractor by lazy { kosmos.authenticationInteractor } private val bouncerInteractor by lazy { kosmos.bouncerInteractor } - private val bouncerViewModel by lazy { kosmos.bouncerViewModel } private lateinit var underTest: PinBouncerViewModel @Before @@ -69,6 +68,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true).asStateFlow(), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, + onIntentionalUserInput = {}, ) overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN) @@ -78,11 +78,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() - assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN) assertThat(pin).isEmpty() assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin) } @@ -98,6 +96,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true).asStateFlow(), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Sim, + onIntentionalUserInput = {}, ) assertThat(underTest.isSimAreaVisible).isTrue() @@ -126,6 +125,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true).asStateFlow(), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Sim, + onIntentionalUserInput = {}, ) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) val hintedPinLength by collectLastValue(underTest.hintedPinLength) @@ -136,20 +136,17 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onPinButtonClicked() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() underTest.onPinButtonClicked(1) - assertThat(message?.text).isEmpty() assertThat(pin).containsExactly(1) } @Test fun onBackspaceButtonClicked() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -158,7 +155,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onBackspaceButtonClicked() - assertThat(message?.text).isEmpty() assertThat(pin).isEmpty() } @@ -183,7 +179,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onBackspaceButtonLongPressed() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -195,7 +190,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onBackspaceButtonLongPressed() - assertThat(message?.text).isEmpty() assertThat(pin).isEmpty() assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -217,7 +211,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onAuthenticateButtonClicked_whenWrong() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -230,7 +223,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateButtonClicked() assertThat(pin).isEmpty() - assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -238,7 +230,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onAuthenticateButtonClicked_correctAfterWrong() = testScope.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -248,13 +239,11 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onPinButtonClicked(4) underTest.onPinButtonClicked(5) // PIN is now wrong! underTest.onAuthenticateButtonClicked() - assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) assertThat(pin).isEmpty() assertThat(authResult).isFalse() // Enter the correct PIN: FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked) - assertThat(message?.text).isEmpty() underTest.onAuthenticateButtonClicked() @@ -277,7 +266,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onAutoConfirm_whenWrong() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) lockDeviceAndOpenPinBouncer() @@ -290,7 +278,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { ) // PIN is now wrong! assertThat(pin).isEmpty() - assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -390,10 +377,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) - val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer sceneInteractor.changeScene(toScene, "reason") - if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 8e2e94716660..a7e98ea34154 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -18,10 +18,16 @@ package com.android.systemui.communal.view.viewmodel import android.app.smartspace.SmartspaceTarget import android.appwidget.AppWidgetProviderInfo +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo import android.content.pm.UserInfo import android.os.UserHandle import android.provider.Settings import android.widget.RemoteViews +import androidx.activity.result.ActivityResultLauncher import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger @@ -39,6 +45,7 @@ import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.view.MediaHost @@ -46,15 +53,19 @@ import com.android.systemui.settings.fakeUserTracker import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -64,6 +75,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost @Mock private lateinit var uiEventLogger: UiEventLogger @Mock private lateinit var providerInfo: AppWidgetProviderInfo + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent> private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -73,6 +86,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { private lateinit var smartspaceRepository: FakeSmartspaceRepository private lateinit var mediaRepository: FakeCommunalMediaRepository + private val testableResources = context.orCreateTestableResources + private lateinit var underTest: CommunalEditModeViewModel @Before @@ -96,6 +111,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { mediaHost, uiEventLogger, logcatLogBuffer("CommunalEditModeViewModelTest"), + kosmos.testDispatcher, ) } @@ -217,7 +233,69 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } + @Test + fun onOpenWidgetPicker_launchesWidgetPickerActivity() { + testScope.runTest { + whenever(packageManager.resolveActivity(any(), anyInt())).then { + ResolveInfo().apply { + activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME } + } + } + + val success = + underTest.onOpenWidgetPicker( + testableResources.resources, + packageManager, + activityResultLauncher + ) + + verify(activityResultLauncher).launch(any()) + assertTrue(success) + } + } + + @Test + fun onOpenWidgetPicker_launcherActivityNotResolved_doesNotLaunchWidgetPickerActivity() { + testScope.runTest { + whenever(packageManager.resolveActivity(any(), anyInt())).thenReturn(null) + + val success = + underTest.onOpenWidgetPicker( + testableResources.resources, + packageManager, + activityResultLauncher + ) + + verify(activityResultLauncher, never()).launch(any()) + assertFalse(success) + } + } + + @Test + fun onOpenWidgetPicker_activityLaunchThrowsException_failure() { + testScope.runTest { + whenever(packageManager.resolveActivity(any(), anyInt())).then { + ResolveInfo().apply { + activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME } + } + } + + whenever(activityResultLauncher.launch(any())) + .thenThrow(ActivityNotFoundException::class.java) + + val success = + underTest.onOpenWidgetPicker( + testableResources.resources, + packageManager, + activityResultLauncher, + ) + + assertFalse(success) + } + } + private companion object { val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name" } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt index 69ff5ab3d84d..b4f87c47a0b0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetInteractionHandlerTest.kt @@ -21,6 +21,8 @@ import android.content.Intent import android.view.View import android.widget.FrameLayout import android.widget.RemoteViews.RemoteResponse +import androidx.core.util.component1 +import androidx.core.util.component2 import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -29,6 +31,7 @@ import com.android.systemui.util.mockito.eq import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.refEq import org.mockito.Mock import org.mockito.Mockito.isNull import org.mockito.Mockito.notNull @@ -62,6 +65,7 @@ class WidgetInteractionHandlerTest : SysuiTestCase() { val parent = FrameLayout(context) val view = CommunalAppWidgetHostView(context) parent.addView(view) + val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view) underTest.onInteraction(view, testIntent, testResponse) @@ -70,6 +74,8 @@ class WidgetInteractionHandlerTest : SysuiTestCase() { eq(testIntent), isNull(), notNull(), + refEq(fillInIntent), + refEq(activityOptions.toBundle()), ) } @@ -78,10 +84,17 @@ class WidgetInteractionHandlerTest : SysuiTestCase() { val parent = FrameLayout(context) val view = View(context) parent.addView(view) + val (fillInIntent, activityOptions) = testResponse.getLaunchOptions(view) underTest.onInteraction(view, testIntent, testResponse) verify(activityStarter) - .startPendingIntentMaybeDismissingKeyguard(eq(testIntent), isNull(), isNull()) + .startPendingIntentMaybeDismissingKeyguard( + eq(testIntent), + isNull(), + isNull(), + refEq(fillInIntent), + refEq(activityOptions.toBundle()), + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt index decbdaf0feee..51f99570b51e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt @@ -26,12 +26,10 @@ import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthR import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import kotlin.test.Test -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() { @@ -59,17 +57,20 @@ class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() { } @Test - fun isSensorUnderDisplay_trueForUdfpsSensorTypes() = + fun isFingerprintCurrentlyAllowedInBouncer_trueForNonUdfpsSensorTypes() = testScope.runTest { - val isSensorUnderDisplay by collectLastValue(underTest.isSensorUnderDisplay) + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + val isFingerprintCurrentlyAllowedInBouncer by + collectLastValue(underTest.isFingerprintCurrentlyAllowedOnBouncer) fingerprintPropertyRepository.supportsUdfps() - assertThat(isSensorUnderDisplay).isTrue() + assertThat(isFingerprintCurrentlyAllowedInBouncer).isFalse() fingerprintPropertyRepository.supportsRearFps() - assertThat(isSensorUnderDisplay).isFalse() + assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue() fingerprintPropertyRepository.supportsSideFps() - assertThat(isSensorUnderDisplay).isFalse() + assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 769caaa8454f..36458ede9506 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -270,12 +270,61 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test + fun transitionValue_canceled_toAnotherState() = + testScope.runTest { + val transitionValuesGone by collectValues(underTest.transitionValue(state = GONE)) + val transitionValuesAod by collectValues(underTest.transitionValue(state = AOD)) + val transitionValuesLs by collectValues(underTest.transitionValue(state = LOCKSCREEN)) + + listOf( + TransitionStep(GONE, AOD, 0f, STARTED), + TransitionStep(GONE, AOD, 0.5f, RUNNING), + TransitionStep(GONE, AOD, 0.5f, CANCELED), + TransitionStep(AOD, LOCKSCREEN, 0.5f, STARTED), + TransitionStep(AOD, LOCKSCREEN, 0.7f, RUNNING), + TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED), + ) + .forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(transitionValuesGone).isEqualTo(listOf(1f, 0.5f, 0f)) + assertThat(transitionValuesAod).isEqualTo(listOf(0f, 0.5f, 0.5f, 0.3f, 0f)) + assertThat(transitionValuesLs).isEqualTo(listOf(0.5f, 0.7f, 1f)) + } + + @Test + fun transitionValue_canceled_backToOriginalState() = + testScope.runTest { + val transitionValuesGone by collectValues(underTest.transitionValue(state = GONE)) + val transitionValuesAod by collectValues(underTest.transitionValue(state = AOD)) + + listOf( + TransitionStep(GONE, AOD, 0f, STARTED), + TransitionStep(GONE, AOD, 0.5f, RUNNING), + TransitionStep(GONE, AOD, 1f, CANCELED), + TransitionStep(AOD, GONE, 0.5f, STARTED), + TransitionStep(AOD, GONE, 0.7f, RUNNING), + TransitionStep(AOD, GONE, 1f, FINISHED), + ) + .forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(transitionValuesGone).isEqualTo(listOf(1f, 0.5f, 0.5f, 0.7f, 1f)) + assertThat(transitionValuesAod).isEqualTo(listOf(0f, 0.5f, 0.5f, 0.3f, 0f)) + } + + @Test fun isInTransitionToAnyState() = testScope.runTest { val inTransition by collectValues(underTest.isInTransitionToAnyState) assertEquals( listOf( + false, true, // The repo is seeded with a transition from OFF to LOCKSCREEN. false, ), @@ -288,6 +337,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -301,6 +351,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -314,6 +365,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -330,6 +382,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, ), @@ -345,6 +398,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -359,6 +413,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -379,6 +434,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -398,6 +454,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt index d4438516a023..0cc0c2fb530b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel 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.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -32,6 +33,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -49,9 +51,7 @@ class AlternateBouncerToGoneTransitionViewModelTest : SysuiTestCase() { } private val testScope = kosmos.testScope private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val underTest by lazy { - kosmos.alternateBouncerToGoneTransitionViewModel - } + private val underTest by lazy { kosmos.alternateBouncerToGoneTransitionViewModel } @Test fun deviceEntryParentViewDisappear() = @@ -73,6 +73,61 @@ class AlternateBouncerToGoneTransitionViewModelTest : SysuiTestCase() { values.forEach { assertThat(it).isEqualTo(0f) } } + @Test + fun lockscreenAlpha() = + testScope.runTest { + val startAlpha = 0.6f + val viewState = ViewStateAccessor(alpha = { startAlpha }) + val alpha by collectLastValue(underTest.lockscreenAlpha(viewState)) + runCurrent() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.25f), + step(0.5f), + step(0.75f), + step(1f), + ), + testScope, + ) + + // Alpha starts at the starting value from ViewStateAccessor. + keyguardTransitionRepository.sendTransitionStep( + step(0f, state = TransitionState.STARTED) + ) + runCurrent() + assertThat(alpha).isEqualTo(startAlpha) + + // Alpha finishes in 200ms out of 500ms, check the alpha at the halfway point. + val progress = 0.2f + keyguardTransitionRepository.sendTransitionStep(step(progress)) + runCurrent() + assertThat(alpha).isEqualTo(0.3f) + + // Alpha ends at 0. + keyguardTransitionRepository.sendTransitionStep(step(1f)) + runCurrent() + assertThat(alpha).isEqualTo(0f) + } + + @Test + fun lockscreenAlpha_zeroInitialAlpha() = + testScope.runTest { + // ViewState starts at 0 alpha. + val viewState = ViewStateAccessor(alpha = { 0f }) + val alpha by collectValues(underTest.lockscreenAlpha(viewState)) + + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.ALTERNATE_BOUNCER, + to = GONE, + testScope + ) + + // Alpha starts and ends at 0. + alpha.forEach { assertThat(it).isEqualTo(0f) } + } + private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep { return TransitionStep( from = KeyguardState.ALTERNATE_BOUNCER, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt index e7aaddd94695..857b9f82f8bc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt @@ -68,9 +68,7 @@ class LockscreenToGoneTransitionViewModelTest : SysuiTestCase() { repository.sendTransitionStep(step(0f)) assertThat(alpha).isEqualTo(0.5f) - repository.sendTransitionStep(step(0.25f)) - assertThat(alpha).isEqualTo(0.25f) - + // Before the halfway point, it will have reached zero repository.sendTransitionStep(step(.5f)) assertThat(alpha).isEqualTo(0f) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt index 0796af065790..409c55144c6a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt @@ -91,27 +91,6 @@ class PrimaryBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() { assertThat(bgViewAlpha).isEqualTo(1f) } - @Test - fun deviceEntryBackgroundViewAlpha_rearFpEnrolled_noUpdates() = - testScope.runTest { - fingerprintPropertyRepository.supportsRearFps() - val bgViewAlpha by collectLastValue(underTest.deviceEntryBackgroundViewAlpha) - keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED)) - assertThat(bgViewAlpha).isNull() - - keyguardTransitionRepository.sendTransitionStep(step(0.5f)) - assertThat(bgViewAlpha).isNull() - - keyguardTransitionRepository.sendTransitionStep(step(.75f)) - assertThat(bgViewAlpha).isNull() - - keyguardTransitionRepository.sendTransitionStep(step(1f)) - assertThat(bgViewAlpha).isNull() - - keyguardTransitionRepository.sendTransitionStep(step(1f, TransitionState.FINISHED)) - assertThat(bgViewAlpha).isNull() - } - private fun step( value: Float, state: TransitionState = TransitionState.RUNNING diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt new file mode 100644 index 000000000000..8e44932fb38e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls + +import android.R +import android.app.smartspace.SmartspaceAction +import android.content.Context +import android.graphics.drawable.Icon +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever + +class MediaTestHelper { + companion object { + /** Returns a list of three mocked recommendations */ + fun getValidRecommendationList(context: Context): List<SmartspaceAction> { + val mediaRecommendationItem = + mock<SmartspaceAction> { + whenever(icon) + .thenReturn( + Icon.createWithResource( + context, + R.drawable.ic_media_play, + ) + ) + } + return listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt new file mode 100644 index 000000000000..6c41bc3c1000 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.data.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.controls.MediaTestHelper +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class MediaDataRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val underTest: MediaDataRepository = kosmos.mediaDataRepository + + @Test + fun setRecommendation() = + testScope.runTest { + val smartspaceData by collectLastValue(underTest.smartspaceMediaData) + val recommendation = SmartspaceMediaData(isActive = true) + + underTest.setRecommendation(recommendation) + + assertThat(smartspaceData).isEqualTo(recommendation) + } + + @Test + fun addAndRemoveMediaData() = + testScope.runTest { + val entries by collectLastValue(underTest.mediaEntries) + + val firstKey = "key1" + val firstData = MediaData().copy(isPlaying = true) + + val secondKey = "key2" + val secondData = MediaData().copy(resumption = true) + + underTest.addMediaEntry(firstKey, firstData) + underTest.addMediaEntry(secondKey, secondData) + underTest.addMediaEntry(firstKey, firstData.copy(isPlaying = false)) + + assertThat(entries!!.size).isEqualTo(2) + assertThat(entries!![firstKey]).isNotEqualTo(firstData) + + underTest.removeMediaEntry(firstKey) + + assertThat(entries!!.size).isEqualTo(1) + assertThat(entries!![secondKey]).isEqualTo(secondData) + } + + @Test + fun setRecommendationInactive() = + testScope.runTest { + kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, true) + val smartspaceData by collectLastValue(underTest.smartspaceMediaData) + val recommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + + underTest.setRecommendation(recommendation) + + assertThat(smartspaceData).isEqualTo(recommendation) + + underTest.setRecommendationInactive(KEY_MEDIA_SMARTSPACE) + + assertThat(smartspaceData).isNotEqualTo(recommendation) + assertThat(smartspaceData!!.isActive).isFalse() + } + + @Test + fun dismissRecommendation() = + testScope.runTest { + val smartspaceData by collectLastValue(underTest.smartspaceMediaData) + val recommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + + underTest.setRecommendation(recommendation) + + assertThat(smartspaceData).isEqualTo(recommendation) + + underTest.dismissSmartspaceRecommendation(KEY_MEDIA_SMARTSPACE) + + assertThat(smartspaceData!!.isActive).isFalse() + } + + companion object { + private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt new file mode 100644 index 000000000000..d39e77da2f55 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.data.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.controls.MediaTestHelper +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class MediaFilterRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val underTest: MediaFilterRepository = kosmos.mediaFilterRepository + + @Test + fun addSelectedUserMediaEntry_activeThenInactivate() = + testScope.runTest { + val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) + + val userMedia = MediaData().copy(active = true) + + underTest.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) + + underTest.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false)) + + assertThat(selectedUserEntries?.get(KEY)).isNotEqualTo(userMedia) + assertThat(selectedUserEntries?.get(KEY)?.active).isFalse() + } + + @Test + fun addSelectedUserMediaEntry_thenRemove_returnsBoolean() = + testScope.runTest { + val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) + + val userMedia = MediaData() + + underTest.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) + + assertThat(underTest.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue() + } + + @Test + fun addSelectedUserMediaEntry_thenRemove_returnsValue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) + + val userMedia = MediaData() + + underTest.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) + + assertThat(underTest.removeSelectedUserMediaEntry(KEY)).isEqualTo(userMedia) + } + + @Test + fun addAllUserMediaEntry_activeThenInactivate() = + testScope.runTest { + val allUserEntries by collectLastValue(underTest.allUserEntries) + + val userMedia = MediaData().copy(active = true) + + underTest.addMediaEntry(KEY, userMedia) + + assertThat(allUserEntries?.get(KEY)).isEqualTo(userMedia) + + underTest.addMediaEntry(KEY, userMedia.copy(active = false)) + + assertThat(allUserEntries?.get(KEY)).isNotEqualTo(userMedia) + assertThat(allUserEntries?.get(KEY)?.active).isFalse() + } + + @Test + fun addAllUserMediaEntry_thenRemove_returnsValue() = + testScope.runTest { + val allUserEntries by collectLastValue(underTest.allUserEntries) + + val userMedia = MediaData() + + underTest.addMediaEntry(KEY, userMedia) + + assertThat(allUserEntries?.get(KEY)).isEqualTo(userMedia) + + assertThat(underTest.removeMediaEntry(KEY)).isEqualTo(userMedia) + } + + @Test + fun addActiveRecommendation_thenInactive() = + testScope.runTest { + val smartspaceMediaData by collectLastValue(underTest.smartspaceMediaData) + + val mediaRecommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + + underTest.setRecommendation(mediaRecommendation) + + assertThat(smartspaceMediaData).isEqualTo(mediaRecommendation) + + underTest.setRecommendation(mediaRecommendation.copy(isActive = false)) + + assertThat(smartspaceMediaData).isNotEqualTo(mediaRecommendation) + assertThat(smartspaceMediaData?.isActive).isFalse() + } + + companion object { + private const val KEY = "KEY" + private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt new file mode 100644 index 000000000000..6e67000b1ab3 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.controls.MediaTestHelper +import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class MediaCarouselInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository + private val underTest: MediaCarouselInteractor = kosmos.mediaCarouselInteractor + + @Test + fun addUserMediaEntry_activeThenInactivate() = + testScope.runTest { + val hasActiveMediaOrRecommendation by + collectLastValue(underTest.hasActiveMediaOrRecommendation) + val hasActiveMedia by collectLastValue(underTest.hasActiveMedia) + val hasAnyMedia by collectLastValue(underTest.hasAnyMedia) + + val userMedia = MediaData().copy(active = true) + + mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(hasActiveMediaOrRecommendation).isTrue() + assertThat(hasActiveMedia).isTrue() + assertThat(hasAnyMedia).isTrue() + + mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false)) + + assertThat(hasActiveMediaOrRecommendation).isFalse() + assertThat(hasActiveMedia).isFalse() + assertThat(hasAnyMedia).isTrue() + } + + @Test + fun addInactiveUserMediaEntry_thenRemove() = + testScope.runTest { + val hasActiveMediaOrRecommendation by + collectLastValue(underTest.hasActiveMediaOrRecommendation) + val hasActiveMedia by collectLastValue(underTest.hasActiveMedia) + val hasAnyMedia by collectLastValue(underTest.hasAnyMedia) + + val userMedia = MediaData().copy(active = false) + + mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(hasActiveMediaOrRecommendation).isFalse() + assertThat(hasActiveMedia).isFalse() + assertThat(hasAnyMedia).isTrue() + + assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue() + + assertThat(hasActiveMediaOrRecommendation).isFalse() + assertThat(hasActiveMedia).isFalse() + assertThat(hasAnyMedia).isFalse() + } + + @Test + fun addActiveRecommendation_inactiveMedia() = + testScope.runTest { + val hasActiveMediaOrRecommendation by + collectLastValue(underTest.hasActiveMediaOrRecommendation) + val hasAnyMediaOrRecommendation by + collectLastValue(underTest.hasAnyMediaOrRecommendation) + kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) + + val userMediaRecommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + val userMedia = MediaData().copy(active = false) + + mediaFilterRepository.setRecommendation(userMediaRecommendation) + + assertThat(hasActiveMediaOrRecommendation).isTrue() + assertThat(hasAnyMediaOrRecommendation).isTrue() + + mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(hasActiveMediaOrRecommendation).isTrue() + assertThat(hasAnyMediaOrRecommendation).isTrue() + } + + @Test + fun addActiveRecommendation_thenInactive() = + testScope.runTest { + val hasActiveMediaOrRecommendation by + collectLastValue(underTest.hasActiveMediaOrRecommendation) + val hasAnyMediaOrRecommendation by + collectLastValue(underTest.hasAnyMediaOrRecommendation) + kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) + + val mediaRecommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + + mediaFilterRepository.setRecommendation(mediaRecommendation) + + assertThat(hasActiveMediaOrRecommendation).isTrue() + assertThat(hasAnyMediaOrRecommendation).isTrue() + + mediaFilterRepository.setRecommendation(mediaRecommendation.copy(isActive = false)) + + assertThat(hasActiveMediaOrRecommendation).isFalse() + assertThat(hasAnyMediaOrRecommendation).isFalse() + } + + @Test + fun addActiveRecommendation_thenInvalid() = + testScope.runTest { + val hasActiveMediaOrRecommendation by + collectLastValue(underTest.hasActiveMediaOrRecommendation) + val hasAnyMediaOrRecommendation by + collectLastValue(underTest.hasAnyMediaOrRecommendation) + kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) + + val mediaRecommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + + mediaFilterRepository.setRecommendation(mediaRecommendation) + + assertThat(hasActiveMediaOrRecommendation).isTrue() + assertThat(hasAnyMediaOrRecommendation).isTrue() + + mediaFilterRepository.setRecommendation( + mediaRecommendation.copy(recommendations = listOf()) + ) + + assertThat(hasActiveMediaOrRecommendation).isFalse() + assertThat(hasAnyMediaOrRecommendation).isFalse() + } + + @Test + fun hasAnyMedia_noMediaSet_returnsFalse() = + testScope.runTest { assertThat(underTest.hasAnyMedia.value).isFalse() } + + @Test + fun hasAnyMediaOrRecommendation_noMediaSet_returnsFalse() = + testScope.runTest { assertThat(underTest.hasAnyMediaOrRecommendation.value).isFalse() } + + @Test + fun hasActiveMedia_noMediaSet_returnsFalse() = + testScope.runTest { assertThat(underTest.hasActiveMedia.value).isFalse() } + + @Test + fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() = + testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() } + + companion object { + private const val KEY = "KEY" + private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt index c2ce39249f9e..f1cd0c843256 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt @@ -185,7 +185,7 @@ class AlarmTileMapperTest : SysuiTestCase() { setOf(QSTileState.UserAction.CLICK), label, null, - QSTileState.SideViewIcon.None, + QSTileState.SideViewIcon.Chevron, QSTileState.EnabledState.ENABLED, Switch::class.qualifiedName ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt index f24723a2a9f3..97a10e68960f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt @@ -33,7 +33,6 @@ import kotlin.coroutines.EmptyCoroutineContext import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock import org.mockito.Mockito.verify /** Test [DataSaverDialogDelegate]. */ @@ -69,7 +68,7 @@ class DataSaverDialogDelegateTest : SysuiTestCase() { fun delegateSetsDialogTitleCorrectly() { val expectedResId = R.string.data_saver_enable_title - dataSaverDialogDelegate.onCreate(sysuiDialog, null) + dataSaverDialogDelegate.beforeCreate(sysuiDialog, null) verify(sysuiDialog).setTitle(eq(expectedResId)) } @@ -78,7 +77,7 @@ class DataSaverDialogDelegateTest : SysuiTestCase() { fun delegateSetsDialogMessageCorrectly() { val expectedResId = R.string.data_saver_description - dataSaverDialogDelegate.onCreate(sysuiDialog, null) + dataSaverDialogDelegate.beforeCreate(sysuiDialog, null) verify(sysuiDialog).setMessage(expectedResId) } @@ -87,7 +86,7 @@ class DataSaverDialogDelegateTest : SysuiTestCase() { fun delegateSetsDialogPositiveButtonCorrectly() { val expectedResId = R.string.data_saver_enable_button - dataSaverDialogDelegate.onCreate(sysuiDialog, null) + dataSaverDialogDelegate.beforeCreate(sysuiDialog, null) verify(sysuiDialog).setPositiveButton(eq(expectedResId), any()) } @@ -96,7 +95,7 @@ class DataSaverDialogDelegateTest : SysuiTestCase() { fun delegateSetsDialogCancelButtonCorrectly() { val expectedResId = R.string.cancel - dataSaverDialogDelegate.onCreate(sysuiDialog, null) + dataSaverDialogDelegate.beforeCreate(sysuiDialog, null) verify(sysuiDialog).setNeutralButton(eq(expectedResId), eq(null)) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt new file mode 100644 index 000000000000..86513006cef1 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.work.domain.interactor + +import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood +import android.testing.LeakCheck +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.utils.leaks.FakeManagedProfileController +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@EnabledOnRavenwood +@RunWith(AndroidJUnit4::class) +class WorkModeTileDataInteractorTest : SysuiTestCase() { + private val controller = FakeManagedProfileController(LeakCheck()) + private val underTest: WorkModeTileDataInteractor = WorkModeTileDataInteractor(controller) + + @Test + fun availability_matchesControllerHasActiveProfiles() = runTest { + val availability by collectLastValue(underTest.availability(TEST_USER)) + + assertThat(availability).isFalse() + + controller.setHasActiveProfile(true) + assertThat(availability).isTrue() + + controller.setHasActiveProfile(false) + assertThat(availability).isFalse() + } + + @Test + fun tileData_whenHasActiveProfile_matchesControllerIsEnabled() = runTest { + controller.setHasActiveProfile(true) + val data by + collectLastValue( + underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)) + ) + + assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java) + assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isFalse() + + controller.isWorkModeEnabled = true + assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java) + assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isTrue() + + controller.isWorkModeEnabled = false + assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java) + assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isFalse() + } + + @Test + fun tileData_matchesControllerHasActiveProfile() = runTest { + val data by + collectLastValue( + underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)) + ) + assertThat(data).isInstanceOf(WorkModeTileModel.NoActiveProfile::class.java) + + controller.setHasActiveProfile(true) + assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java) + + controller.setHasActiveProfile(false) + assertThat(data).isInstanceOf(WorkModeTileModel.NoActiveProfile::class.java) + } + + private companion object { + val TEST_USER = UserHandle.of(1)!! + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt new file mode 100644 index 000000000000..8a63e2c8800f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.work.domain.interactor + +import android.platform.test.annotations.EnabledOnRavenwood +import android.provider.Settings +import android.testing.LeakCheck +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject +import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.utils.leaks.FakeManagedProfileController +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@EnabledOnRavenwood +@RunWith(AndroidJUnit4::class) +class WorkModeTileUserActionInteractorTest : SysuiTestCase() { + + private val inputHandler = FakeQSTileIntentUserInputHandler() + private val profileController = FakeManagedProfileController(LeakCheck()) + + private val underTest = + WorkModeTileUserActionInteractor( + profileController, + inputHandler, + ) + + @Test + fun handleClickWhenEnabled() = runTest { + val wasEnabled = true + profileController.isWorkModeEnabled = wasEnabled + + underTest.handleInput( + QSTileInputTestKtx.click(WorkModeTileModel.HasActiveProfile(wasEnabled)) + ) + + assertThat(profileController.isWorkModeEnabled).isEqualTo(!wasEnabled) + } + + @Test + fun handleClickWhenDisabled() = runTest { + val wasEnabled = false + profileController.isWorkModeEnabled = wasEnabled + + underTest.handleInput( + QSTileInputTestKtx.click(WorkModeTileModel.HasActiveProfile(wasEnabled)) + ) + + assertThat(profileController.isWorkModeEnabled).isEqualTo(!wasEnabled) + } + + @Test + fun handleClickWhenUnavailable() = runTest { + val wasEnabled = false + profileController.isWorkModeEnabled = wasEnabled + + underTest.handleInput(QSTileInputTestKtx.click(WorkModeTileModel.NoActiveProfile)) + + assertThat(profileController.isWorkModeEnabled).isEqualTo(wasEnabled) + } + + @Test + fun handleLongClickWhenDisabled() = runTest { + val enabled = false + + underTest.handleInput( + QSTileInputTestKtx.longClick(WorkModeTileModel.HasActiveProfile(enabled)) + ) + + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { + assertThat(it.intent.action).isEqualTo(Settings.ACTION_MANAGED_PROFILE_SETTINGS) + } + } + + @Test + fun handleLongClickWhenEnabled() = runTest { + val enabled = true + + underTest.handleInput( + QSTileInputTestKtx.longClick(WorkModeTileModel.HasActiveProfile(enabled)) + ) + + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { + assertThat(it.intent.action).isEqualTo(Settings.ACTION_MANAGED_PROFILE_SETTINGS) + } + } + + @Test + fun handleLongClickWhenUnavailable() = runTest { + underTest.handleInput(QSTileInputTestKtx.longClick(WorkModeTileModel.NoActiveProfile)) + + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledNoInputs() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt index 3c0ab240cbba..27c4ec125b59 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt @@ -27,9 +27,17 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dump.DumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.qs.QSImpl import com.android.systemui.qs.dagger.QSComponent import com.android.systemui.qs.dagger.QSSceneComponent +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture @@ -41,8 +49,6 @@ import com.google.common.truth.Truth.assertThat import java.util.Locale import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -57,8 +63,9 @@ import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) class QSSceneAdapterImplTest : SysuiTestCase() { - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val kosmos = Kosmos().apply { testCase = this@QSSceneAdapterImplTest } + private val testDispatcher = kosmos.testDispatcher + private val testScope = kosmos.testScope private val qsImplProvider = object : Provider<QSImpl> { @@ -107,10 +114,15 @@ class QSSceneAdapterImplTest : SysuiTestCase() { } } + private val shadeInteractor = kosmos.shadeInteractor + private val dumpManager = mock<DumpManager>() + private val underTest = QSSceneAdapterImpl( qsSceneComponentFactory, qsImplProvider, + shadeInteractor, + dumpManager, testDispatcher, testScope.backgroundScope, configurationInteractor, @@ -158,12 +170,6 @@ class QSSceneAdapterImplTest : SysuiTestCase() { ) verify(this).setListening(false) verify(this).setExpanded(false) - verify(this) - .setTransitionToFullShadeProgress( - /* isTransitioningToFullShade= */ false, - /* qsTransitionFraction= */ 1f, - /* qsSquishinessFraction = */ 1f, - ) } } @@ -187,13 +193,7 @@ class QSSceneAdapterImplTest : SysuiTestCase() { /* squishinessFraction= */ 1f, ) verify(this).setListening(true) - verify(this).setExpanded(true) - verify(this) - .setTransitionToFullShadeProgress( - /* isTransitioningToFullShade= */ false, - /* qsTransitionFraction= */ 1f, - /* qsSquishinessFraction = */ 1f, - ) + verify(this).setExpanded(false) } } @@ -218,12 +218,6 @@ class QSSceneAdapterImplTest : SysuiTestCase() { ) verify(this).setListening(true) verify(this).setExpanded(true) - verify(this) - .setTransitionToFullShadeProgress( - /* isTransitioningToFullShade= */ false, - /* qsTransitionFraction= */ 1f, - /* qsSquishinessFraction = */ 1f, - ) } } @@ -249,12 +243,6 @@ class QSSceneAdapterImplTest : SysuiTestCase() { ) verify(this).setListening(true) verify(this).setExpanded(true) - verify(this) - .setTransitionToFullShadeProgress( - /* isTransitioningToFullShade= */ false, - /* qsTransitionFraction= */ 1f, - /* qsSquishinessFraction = */ 1f, - ) } } @@ -268,7 +256,7 @@ class QSSceneAdapterImplTest : SysuiTestCase() { runCurrent() clearInvocations(qsImpl!!) - underTest.setState(QSSceneAdapter.State.Unsquishing(squishiness)) + underTest.setState(QSSceneAdapter.State.UnsquishingQQS(squishiness)) with(qsImpl!!) { verify(this).setQsVisible(true) verify(this) @@ -279,13 +267,7 @@ class QSSceneAdapterImplTest : SysuiTestCase() { /* squishinessFraction= */ squishiness, ) verify(this).setListening(true) - verify(this).setExpanded(true) - verify(this) - .setTransitionToFullShadeProgress( - /* isTransitioningToFullShade= */ false, - /* qsTransitionFraction= */ 1f, - /* qsSquishinessFraction = */ squishiness, - ) + verify(this).setExpanded(false) } } @@ -497,4 +479,21 @@ class QSSceneAdapterImplTest : SysuiTestCase() { verify(qsImpl!!).applyBottomNavBarToCustomizerPadding(navBarHeight) } + + @Test + fun dispatchSplitShade() = + testScope.runTest { + val shadeRepository = kosmos.fakeShadeRepository + shadeRepository.setShadeMode(ShadeMode.Single) + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + verify(qsImpl!!).setInSplitShade(false) + + shadeRepository.setShadeMode(ShadeMode.Split) + runCurrent() + verify(qsImpl!!).setInSplitShade(true) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt index e281383e6250..ebd65fdcd538 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt @@ -49,9 +49,16 @@ class QSSceneAdapterTest : SysuiTestCase() { } @Test - fun unsquishing_expansionSameAsQQS() { + fun unsquishingQQS_expansionSameAsQQS() { val squishiness = 0.6f - assertThat(QSSceneAdapter.State.Unsquishing(squishiness).expansion) + assertThat(QSSceneAdapter.State.UnsquishingQQS(squishiness).expansion) .isEqualTo(QSSceneAdapter.State.QQS.expansion) } + + @Test + fun unsquishingQS_expansionSameAsQS() { + val squishiness = 0.6f + assertThat(QSSceneAdapter.State.UnsquishingQS(squishiness).expansion) + .isEqualTo(QSSceneAdapter.State.QS.expansion) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index cc66f8b2f387..f018cc189a5e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -51,6 +51,8 @@ import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository +import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.phone.CentralSurfaces @@ -175,10 +177,12 @@ class SceneContainerStartableTest : SysuiTestCase() { transitionStateFlow.value = ObservableTransitionState.Idle(Scenes.Gone) assertThat(isVisible).isFalse() - kosmos.headsUpNotificationRepository.hasPinnedHeadsUp.value = true + kosmos.headsUpNotificationRepository.activeHeadsUpRows.value = + buildNotificationRows(isPinned = true) assertThat(isVisible).isTrue() - kosmos.headsUpNotificationRepository.hasPinnedHeadsUp.value = false + kosmos.headsUpNotificationRepository.activeHeadsUpRows.value = + buildNotificationRows(isPinned = false) assertThat(isVisible).isFalse() } @@ -1070,4 +1074,17 @@ class SceneContainerStartableTest : SysuiTestCase() { return transitionStateFlow } + + private fun buildNotificationRows(isPinned: Boolean = false): Set<HeadsUpRowRepository> = + setOf( + fakeHeadsUpRowRepository(key = "0", isPinned = isPinned), + fakeHeadsUpRowRepository(key = "1", isPinned = isPinned), + fakeHeadsUpRowRepository(key = "2", isPinned = isPinned), + fakeHeadsUpRowRepository(key = "3", isPinned = isPinned), + ) + + private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean) = + FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply { + this.isPinned.value = isPinned + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 1c5496142fec..d1c4ec3ddacf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -95,7 +95,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { scope = testScope.backgroundScope, ) - private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() }) + private val qsSceneAdapter = FakeQSSceneAdapter({ mock() }) private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel @@ -122,7 +122,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { applicationScope = testScope.backgroundScope, deviceEntryInteractor = deviceEntryInteractor, shadeHeaderViewModel = shadeHeaderViewModel, - qsSceneAdapter = qsFlexiglassAdapter, + qsSceneAdapter = qsSceneAdapter, notifications = kosmos.notificationsPlaceholderViewModel, mediaDataManager = mediaDataManager, shadeInteractor = kosmos.shadeInteractor, @@ -279,6 +279,20 @@ class ShadeSceneViewModelTest : SysuiTestCase() { } @Test + fun upTransitionSceneKey_customizing_noTransition() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + + qsSceneAdapter.setCustomizing(true) + assertThat( + destinationScenes!! + .keys + .filterIsInstance<Swipe>() + .filter { it.direction == SwipeDirection.Up } + ).isEmpty() + } + + @Test fun shadeMode() = testScope.runTest { val shadeMode by collectLastValue(underTest.shadeMode) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt index 2689fc111142..94539a39869e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt @@ -22,7 +22,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -31,6 +30,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos @@ -64,7 +64,7 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { @Test fun updateBounds() = testScope.runTest { - val bounds by collectLastValue(appearanceViewModel.stackBounds) + val clipping by collectLastValue(appearanceViewModel.stackClipping) val top = 200f val left = 0f @@ -76,15 +76,8 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { right = right, bottom = bottom ) - assertThat(bounds) - .isEqualTo( - NotificationContainerBounds( - left = left, - top = top, - right = right, - bottom = bottom - ) - ) + assertThat(clipping?.bounds) + .isEqualTo(StackBounds(left = left, top = top, right = right, bottom = bottom)) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt new file mode 100644 index 000000000000..bba9991883f5 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.statusbar.notification.domain.interactor + +import android.platform.test.annotations.EnableFlags +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.statusbar.notification.data.repository.FakeHeadsUpRowRepository +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor +import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository +import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications +import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) +class HeadsUpNotificationInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val repository = kosmos.headsUpNotificationRepository + + private val underTest = kosmos.headsUpNotificationInteractor + + @Test + fun hasPinnedRows_emptyList_false() = + testScope.runTest { + val hasPinnedRows by collectLastValue(underTest.hasPinnedRows) + + assertThat(hasPinnedRows).isFalse() + } + + @Test + fun hasPinnedRows_noPinnedRows_false() = + testScope.runTest { + val hasPinnedRows by collectLastValue(underTest.hasPinnedRows) + // WHEN no pinned rows are set + repository.setNotifications( + fakeHeadsUpRowRepository("key 0"), + fakeHeadsUpRowRepository("key 1"), + fakeHeadsUpRowRepository("key 2"), + ) + runCurrent() + + // THEN hasPinnedRows is false + assertThat(hasPinnedRows).isFalse() + } + + @Test + fun hasPinnedRows_hasPinnedRows_true() = + testScope.runTest { + val hasPinnedRows by collectLastValue(underTest.hasPinnedRows) + // WHEN a pinned rows is set + repository.setNotifications( + fakeHeadsUpRowRepository("key 0", isPinned = true), + fakeHeadsUpRowRepository("key 1"), + fakeHeadsUpRowRepository("key 2"), + ) + runCurrent() + + // THEN hasPinnedRows is true + assertThat(hasPinnedRows).isTrue() + } + + @Test + fun hasPinnedRows_rowGetsPinned_true() = + testScope.runTest { + val hasPinnedRows by collectLastValue(underTest.hasPinnedRows) + // GIVEN no rows are pinned + val rows = + arrayListOf( + fakeHeadsUpRowRepository("key 0"), + fakeHeadsUpRowRepository("key 1"), + fakeHeadsUpRowRepository("key 2"), + ) + repository.setNotifications(rows) + runCurrent() + + // WHEN a row gets pinned + rows[0].isPinned.value = true + runCurrent() + + // THEN hasPinnedRows updates to true + assertThat(hasPinnedRows).isTrue() + } + + @Test + fun hasPinnedRows_rowGetsUnPinned_false() = + testScope.runTest { + val hasPinnedRows by collectLastValue(underTest.hasPinnedRows) + // GIVEN one row is pinned + val rows = + arrayListOf( + fakeHeadsUpRowRepository("key 0", isPinned = true), + fakeHeadsUpRowRepository("key 1"), + fakeHeadsUpRowRepository("key 2"), + ) + repository.setNotifications(rows) + runCurrent() + + // THEN that row gets unpinned + rows[0].isPinned.value = false + runCurrent() + + // THEN hasPinnedRows updates to false + assertThat(hasPinnedRows).isFalse() + } + + @Test + fun pinnedRows_noRows_isEmpty() = + testScope.runTest { + val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows) + + assertThat(pinnedHeadsUpRows).isEmpty() + } + + @Test + fun pinnedRows_noPinnedRows_isEmpty() = + testScope.runTest { + val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows) + // WHEN no rows are pinned + repository.setNotifications( + fakeHeadsUpRowRepository("key 0"), + fakeHeadsUpRowRepository("key 1"), + fakeHeadsUpRowRepository("key 2"), + ) + runCurrent() + + // THEN all rows are filtered + assertThat(pinnedHeadsUpRows).isEmpty() + } + + @Test + fun pinnedRows_hasPinnedRows_containsPinnedRows() = + testScope.runTest { + val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows) + // WHEN some rows are pinned + val rows = + arrayListOf( + fakeHeadsUpRowRepository("key 0", isPinned = true), + fakeHeadsUpRowRepository("key 1", isPinned = true), + fakeHeadsUpRowRepository("key 2"), + ) + repository.setNotifications(rows) + runCurrent() + + // THEN the unpinned rows are filtered + assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1]) + } + + @Test + fun pinnedRows_rowGetsPinned_containsPinnedRows() = + testScope.runTest { + val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows) + // GIVEN some rows are pinned + val rows = + arrayListOf( + fakeHeadsUpRowRepository("key 0", isPinned = true), + fakeHeadsUpRowRepository("key 1", isPinned = true), + fakeHeadsUpRowRepository("key 2"), + ) + repository.setNotifications(rows) + runCurrent() + + // WHEN all rows gets pinned + rows[2].isPinned.value = true + runCurrent() + + // THEN no rows are filtered + assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1], rows[2]) + } + + @Test + fun pinnedRows_allRowsPinned_containsAllRows() = + testScope.runTest { + val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows) + // WHEN all rows are pinned + val rows = + arrayListOf( + fakeHeadsUpRowRepository("key 0", isPinned = true), + fakeHeadsUpRowRepository("key 1", isPinned = true), + fakeHeadsUpRowRepository("key 2", isPinned = true), + ) + repository.setNotifications(rows) + runCurrent() + + // THEN no rows are filtered + assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1], rows[2]) + } + + @Test + fun pinnedRows_rowGetsUnPinned_containsPinnedRows() = + testScope.runTest { + val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows) + // GIVEN all rows are pinned + val rows = + arrayListOf( + fakeHeadsUpRowRepository("key 0", isPinned = true), + fakeHeadsUpRowRepository("key 1", isPinned = true), + fakeHeadsUpRowRepository("key 2", isPinned = true), + ) + repository.setNotifications(rows) + runCurrent() + + // WHEN a row gets unpinned + rows[0].isPinned.value = false + runCurrent() + + // THEN the unpinned row is filtered + assertThat(pinnedHeadsUpRows).containsExactly(rows[1], rows[2]) + } + + @Test + fun pinnedRows_rowGetsPinnedAndUnPinned_containsTheSameInstance() = + testScope.runTest { + val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows) + + val rows = + arrayListOf( + fakeHeadsUpRowRepository("key 0"), + fakeHeadsUpRowRepository("key 1"), + fakeHeadsUpRowRepository("key 2"), + ) + repository.setNotifications(rows) + runCurrent() + + rows[0].isPinned.value = true + runCurrent() + assertThat(pinnedHeadsUpRows).containsExactly(rows[0]) + + rows[0].isPinned.value = false + runCurrent() + assertThat(pinnedHeadsUpRows).isEmpty() + + rows[0].isPinned.value = true + runCurrent() + assertThat(pinnedHeadsUpRows).containsExactly(rows[0]) + } + + private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) = + FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply { + this.isPinned.value = isPinned + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt index ffe6e6df6b48..e3fa89c5760d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt @@ -19,10 +19,13 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds +import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test @@ -30,10 +33,9 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class NotificationStackAppearanceInteractorTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = testKosmos() private val testScope = kosmos.testScope private val underTest = kosmos.notificationStackAppearanceInteractor @@ -43,29 +45,39 @@ class NotificationStackAppearanceInteractorTest : SysuiTestCase() { val stackBounds by collectLastValue(underTest.stackBounds) val bounds1 = - NotificationContainerBounds( + StackBounds( top = 100f, bottom = 200f, - isAnimated = true, ) underTest.setStackBounds(bounds1) assertThat(stackBounds).isEqualTo(bounds1) val bounds2 = - NotificationContainerBounds( + StackBounds( top = 200f, bottom = 300f, - isAnimated = false, ) underTest.setStackBounds(bounds2) assertThat(stackBounds).isEqualTo(bounds2) } + @Test + fun stackRounding() = + testScope.runTest { + val stackRounding by collectLastValue(underTest.stackRounding) + + kosmos.shadeRepository.setShadeMode(ShadeMode.Single) + assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = false)) + + kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = true)) + } + @Test(expected = IllegalStateException::class) fun setStackBounds_withImproperBounds_throwsException() = testScope.runTest { underTest.setStackBounds( - NotificationContainerBounds( + StackBounds( top = 100f, bottom = 99f, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt index 693de55211f8..2ccc8b44eff8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt @@ -22,6 +22,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor +import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -36,9 +37,9 @@ class NotificationsPlaceholderViewModelTest : SysuiTestCase() { fun onBoundsChanged_setsNotificationContainerBounds() { underTest.onBoundsChanged(left = 5f, top = 5f, right = 5f, bottom = 5f) assertThat(kosmos.keyguardInteractor.notificationContainerBounds.value) - .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f)) + .isEqualTo(NotificationContainerBounds(top = 5f, bottom = 5f)) assertThat(kosmos.notificationStackAppearanceInteractor.stackBounds.value) - .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f)) + .isEqualTo(StackBounds(left = 5f, top = 5f, right = 5f, bottom = 5f)) } @Test fun onContentTopChanged_setsContentTop() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index 53a8e5dbda32..5256bb956bc4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -720,6 +720,59 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun alphaDoesNotUpdateWhileGoneTransitionIsRunning() = + testScope.runTest { + val viewState = ViewStateAccessor() + val alpha by collectLastValue(underTest.keyguardAlpha(viewState)) + + showLockscreen() + // GONE transition gets to 90% complete + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + transitionState = TransitionState.STARTED, + value = 0f, + ) + ) + runCurrent() + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + transitionState = TransitionState.RUNNING, + value = 0.9f, + ) + ) + runCurrent() + + // At this point, alpha should be zero + assertThat(alpha).isEqualTo(0f) + + // An attempt to override by the shade should be ignored + shadeRepository.setQsExpansion(0.5f) + assertThat(alpha).isEqualTo(0f) + } + + @Test + fun alphaWhenGoneIsSetToOne() = + testScope.runTest { + val viewState = ViewStateAccessor() + val alpha by collectLastValue(underTest.keyguardAlpha(viewState)) + + showLockscreen() + + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + testScope + ) + keyguardRepository.setStatusBarState(StatusBarState.SHADE) + + assertThat(alpha).isEqualTo(1f) + } + + @Test fun shadeCollapseFadeIn() = testScope.runTest { val fadeIn by collectValues(underTest.shadeCollapseFadeIn) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt index 8aa0e3fc4d23..c8062fb4e724 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt @@ -16,12 +16,15 @@ package com.android.systemui.statusbar.phone +import android.app.ActivityOptions import android.app.PendingIntent import android.content.Intent +import android.os.Bundle import android.os.RemoteException import android.os.UserHandle import android.view.View import android.widget.FrameLayout +import android.window.SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor @@ -48,6 +51,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever @@ -173,6 +177,53 @@ class ActivityStarterImplTest : SysuiTestCase() { ) } + fun startPendingIntentDismissingKeyguard_fillInIntentAndExtraOptions_sendAndReturnResult() { + val pendingIntent = mock(PendingIntent::class.java) + val fillInIntent = mock(Intent::class.java) + val parent = FrameLayout(context) + val view = + object : View(context), LaunchableView { + override fun setShouldBlockVisibilityChanges(block: Boolean) {} + } + parent.addView(view) + val controller = ActivityTransitionAnimator.Controller.fromView(view) + whenever(pendingIntent.isActivity).thenReturn(true) + whenever(keyguardStateController.isShowing).thenReturn(true) + whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(true) + whenever(activityIntentHelper.wouldPendingShowOverLockscreen(eq(pendingIntent), anyInt())) + .thenReturn(false) + + // extra activity options to set on pending intent + val activityOptions = mock(ActivityOptions::class.java) + activityOptions.splashScreenStyle = SPLASH_SCREEN_STYLE_SOLID_COLOR + activityOptions.isPendingIntentBackgroundActivityLaunchAllowedByPermission = false + val bundleCaptor = argumentCaptor<Bundle>() + + underTest.startPendingIntentMaybeDismissingKeyguard( + intent = pendingIntent, + animationController = controller, + intentSentUiThreadCallback = null, + fillInIntent = fillInIntent, + extraOptions = activityOptions.toBundle(), + ) + mainExecutor.runAllReady() + + // Fill-in intent is passed and options contain extra values specified + verify(pendingIntent) + .sendAndReturnResult( + eq(context), + eq(0), + eq(fillInIntent), + nullable(), + nullable(), + nullable(), + bundleCaptor.capture() + ) + val options = ActivityOptions.fromBundle(bundleCaptor.value) + assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission).isFalse() + assertThat(options.splashScreenStyle).isEqualTo(SPLASH_SCREEN_STYLE_SOLID_COLOR) + } + @Test fun startPendingIntentDismissingKeyguard_associatedView_getAnimatorController() { val pendingIntent = mock(PendingIntent::class.java) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java index 91699381ae7a..781a9a85edb3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java @@ -46,7 +46,7 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.DozeInteractor; import com.android.systemui.shade.NotificationShadeWindowViewController; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.StatusBarState; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt index be63301e5749..30564bb6eb84 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt @@ -60,7 +60,7 @@ class AvalancheControllerTest : SysuiTestCase() { private val mGlobalSettings = FakeGlobalSettings() private val mSystemClock = FakeSystemClock() private val mExecutor = FakeExecutor(mSystemClock) - private var testableHeadsUpManager: BaseHeadsUpManager? = null + private lateinit var testableHeadsUpManager: BaseHeadsUpManager @Before fun setUp() { @@ -88,20 +88,15 @@ class AvalancheControllerTest : SysuiTestCase() { } private fun createHeadsUpEntry(id: Int): BaseHeadsUpManager.HeadsUpEntry { - val entry = testableHeadsUpManager!!.createHeadsUpEntry() - - entry.setEntry( + return testableHeadsUpManager.createHeadsUpEntry( NotificationEntryBuilder() .setSbn(HeadsUpManagerTestUtil.createSbn(id, Notification.Builder(mContext, ""))) .build() ) - return entry } private fun createFsiHeadsUpEntry(id: Int): BaseHeadsUpManager.HeadsUpEntry { - val entry = testableHeadsUpManager!!.createHeadsUpEntry() - entry.setEntry(createFullScreenIntentEntry(id, mContext)) - return entry + return testableHeadsUpManager.createHeadsUpEntry(createFullScreenIntentEntry(id, mContext)) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java index ed0d272cd848..3dc449514699 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java @@ -38,7 +38,6 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; import android.app.Person; -import android.content.Intent; import android.testing.TestableLooper; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -498,16 +497,16 @@ public class BaseHeadsUpManagerTest extends SysuiTestCase { public void testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() { final BaseHeadsUpManager hum = createHeadsUpManager(); - final BaseHeadsUpManager.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry(); - ongoingCall.setEntry(new NotificationEntryBuilder() - .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0, - new Notification.Builder(mContext, "") - .setCategory(Notification.CATEGORY_CALL) - .setOngoing(true))) - .build()); + final BaseHeadsUpManager.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry( + new NotificationEntryBuilder() + .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0, + new Notification.Builder(mContext, "") + .setCategory(Notification.CATEGORY_CALL) + .setOngoing(true))) + .build()); - final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(); - activeRemoteInput.setEntry(HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext)); + final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry( + HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext)); activeRemoteInput.mRemoteInputActive = true; assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0); @@ -518,18 +517,18 @@ public class BaseHeadsUpManagerTest extends SysuiTestCase { public void testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() { final BaseHeadsUpManager hum = createHeadsUpManager(); - final BaseHeadsUpManager.HeadsUpEntry incomingCall = hum.new HeadsUpEntry(); final Person person = new Person.Builder().setName("person").build(); final PendingIntent intent = mock(PendingIntent.class); - incomingCall.setEntry(new NotificationEntryBuilder() - .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0, - new Notification.Builder(mContext, "") - .setStyle(Notification.CallStyle - .forIncomingCall(person, intent, intent)))) - .build()); - - final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(); - activeRemoteInput.setEntry(HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext)); + final BaseHeadsUpManager.HeadsUpEntry incomingCall = hum.new HeadsUpEntry( + new NotificationEntryBuilder() + .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0, + new Notification.Builder(mContext, "") + .setStyle(Notification.CallStyle + .forIncomingCall(person, intent, intent)))) + .build()); + + final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry( + HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext)); activeRemoteInput.mRemoteInputActive = true; assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0); @@ -541,8 +540,7 @@ public class BaseHeadsUpManagerTest extends SysuiTestCase { final BaseHeadsUpManager hum = createHeadsUpManager(); // Needs full screen intent in order to be pinned - final BaseHeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry(); - entryToPin.setEntry( + final BaseHeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry( HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext)); // Note: the standard way to show a notification would be calling showNotification rather diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java index d8f77f054b49..3c9dc6345d31 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java @@ -54,9 +54,10 @@ class TestableHeadsUpManager extends BaseHeadsUpManager { mStickyForSomeTimeAutoDismissTime = BaseHeadsUpManagerTest.TEST_STICKY_AUTO_DISMISS_TIME; } + @NonNull @Override - protected HeadsUpEntry createHeadsUpEntry() { - mLastCreatedEntry = spy(super.createHeadsUpEntry()); + protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { + mLastCreatedEntry = spy(super.createHeadsUpEntry(entry)); return mLastCreatedEntry; } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt new file mode 100644 index 000000000000..a5ad3c325e51 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.DisposableHandle +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DisposableHandlesTest : SysuiTestCase() { + @Test + fun disposeWorksOnce() { + var handleDisposeCount = 0 + val underTest = DisposableHandles() + + // Add a handle + underTest += DisposableHandle { handleDisposeCount++ } + + // dispose() calls dispose() on children + underTest.dispose() + assertThat(handleDisposeCount).isEqualTo(1) + + // Once disposed, children are not disposed again + underTest.dispose() + assertThat(handleDisposeCount).isEqualTo(1) + } + + @Test + fun replaceCallsDispose() { + var handleDisposeCount1 = 0 + var handleDisposeCount2 = 0 + val underTest = DisposableHandles() + val handle1 = DisposableHandle { handleDisposeCount1++ } + val handle2 = DisposableHandle { handleDisposeCount2++ } + + // First add handle1 + underTest += handle1 + + // replace() calls dispose() on existing children + underTest.replaceAll(handle2) + assertThat(handleDisposeCount1).isEqualTo(1) + assertThat(handleDisposeCount2).isEqualTo(0) + + // Once disposed, replaced children are not disposed again + underTest.dispose() + assertThat(handleDisposeCount1).isEqualTo(1) + assertThat(handleDisposeCount2).isEqualTo(1) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt index 3d936545bbb3..5358a6dbb476 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt @@ -200,6 +200,15 @@ class AudioVolumeInteractorTest : SysuiTestCase() { } } + @Test + fun alarmStream_isNotMutable() { + with(kosmos) { + val isMutable = underTest.isMutable(AudioStream(AudioManager.STREAM_ALARM)) + + assertThat(isMutable).isFalse() + } + } + private companion object { val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt index 471c8d851879..8e925573d40a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.component.bottombar.ui.viewmodel +import android.app.ActivityManager import android.content.Intent import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -23,6 +24,7 @@ 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.plugins.ActivityStarter import com.android.systemui.plugins.activityStarter import com.android.systemui.testKosmos import com.android.systemui.util.mockito.capture @@ -49,6 +51,8 @@ class BottomBarViewModelTest : SysuiTestCase() { @Captor private lateinit var intentCaptor: ArgumentCaptor<Intent> + @Captor private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback> + private val kosmos = testKosmos() private lateinit var underTest: BottomBarViewModel @@ -80,10 +84,13 @@ class BottomBarViewModelTest : SysuiTestCase() { runCurrent() + verify(activityStarter).startActivity(capture(intentCaptor), eq(true), + capture(activityStartedCaptor)) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SOUND_SETTINGS) + + activityStartedCaptor.value.onActivityStarted(ActivityManager.START_SUCCESS) val volumePanelState by collectLastValue(volumePanelViewModel.volumePanelState) assertThat(volumePanelState!!.isVisible).isFalse() - verify(activityStarter).startActivity(capture(intentCaptor), eq(true)) - assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SOUND_SETTINGS) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt new file mode 100644 index 000000000000..b5c580978737 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor + +import android.os.Handler +import android.testing.TestableLooper +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.testCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.volume.localMediaController +import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.mediaOutputInteractor +import com.android.systemui.volume.remoteMediaController +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class MediaDeviceSessionInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private lateinit var underTest: MediaDeviceSessionInteractor + + @Before + fun setup() { + with(kosmos) { + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + underTest = + MediaDeviceSessionInteractor( + testScope.testScheduler, + Handler(TestableLooper.get(kosmos.testCase).looper), + mediaControllerRepository, + ) + } + } + + @Test + fun playbackInfo_returnsPlaybackInfo() { + with(kosmos) { + testScope.runTest { + val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession) + runCurrent() + val info by collectLastValue(underTest.playbackInfo(session!!)) + runCurrent() + + assertThat(info).isEqualTo(localMediaController.playbackInfo) + } + } + } + + @Test + fun playbackState_returnsPlaybackState() { + with(kosmos) { + testScope.runTest { + val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession) + runCurrent() + val state by collectLastValue(underTest.playbackState(session!!)) + runCurrent() + + assertThat(state).isEqualTo(localMediaController.playbackState) + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt index dcf635e622f4..6f7f20b47199 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt @@ -29,9 +29,10 @@ import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.localMediaController import com.android.systemui.volume.localMediaRepository -import com.android.systemui.volume.mediaController import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.mediaDeviceSessionInteractor import com.android.systemui.volume.mediaOutputActionsInteractor import com.android.systemui.volume.mediaOutputInteractor import com.android.systemui.volume.panel.volumePanelViewModel @@ -63,6 +64,7 @@ class MediaOutputViewModelTest : SysuiTestCase() { testScope.backgroundScope, volumePanelViewModel, mediaOutputActionsInteractor, + mediaDeviceSessionInteractor, mediaOutputInteractor, ) @@ -74,11 +76,11 @@ class MediaOutputViewModelTest : SysuiTestCase() { ) } - whenever(mediaController.packageName).thenReturn("test.pkg") - whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) - whenever(mediaController.playbackState).then { playbackStateBuilder.build() } + whenever(localMediaController.packageName).thenReturn("test.pkg") + whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) + whenever(localMediaController.playbackState).then { playbackStateBuilder.build() } - mediaControllerRepository.setActiveLocalMediaController(mediaController) + mediaControllerRepository.setActiveSessions(listOf(localMediaController)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt index 1ed7f5d04622..2f69942aa459 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt @@ -32,8 +32,8 @@ import com.android.systemui.media.spatializerRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.localMediaController import com.android.systemui.volume.localMediaRepository -import com.android.systemui.volume.mediaController import com.android.systemui.volume.mediaControllerRepository import com.android.systemui.volume.panel.component.spatial.spatialAudioComponentInteractor import com.google.common.truth.Truth.assertThat @@ -66,11 +66,11 @@ class SpatialAudioAvailabilityCriteriaTest : SysuiTestCase() { } ) - whenever(mediaController.packageName).thenReturn("test.pkg") - whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) - whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build()) + whenever(localMediaController.packageName).thenReturn("test.pkg") + whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) + whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build()) - mediaControllerRepository.setActiveLocalMediaController(mediaController) + mediaControllerRepository.setActiveSessions(listOf(localMediaController)) underTest = SpatialAudioAvailabilityCriteria(spatialAudioComponentInteractor) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt index 281b03d69536..e36ae60ebe7d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt @@ -34,8 +34,8 @@ import com.android.systemui.media.spatializerRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.localMediaController import com.android.systemui.volume.localMediaRepository -import com.android.systemui.volume.mediaController import com.android.systemui.volume.mediaControllerRepository import com.android.systemui.volume.mediaOutputInteractor import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioAvailabilityModel @@ -70,11 +70,11 @@ class SpatialAudioComponentInteractorTest : SysuiTestCase() { } ) - whenever(mediaController.packageName).thenReturn("test.pkg") - whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) - whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build()) + whenever(localMediaController.packageName).thenReturn("test.pkg") + whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) + whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build()) - mediaControllerRepository.setActiveLocalMediaController(mediaController) + mediaControllerRepository.setActiveSessions(listOf(localMediaController)) underTest = SpatialAudioComponentInteractor( diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java index 1126ec3382a4..072ec9986c61 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java @@ -17,6 +17,7 @@ package com.android.systemui.plugins; import android.annotation.Nullable; import android.app.PendingIntent; import android.content.Intent; +import android.os.Bundle; import android.os.UserHandle; import android.view.View; @@ -67,6 +68,17 @@ public interface ActivityStarter { @Nullable ActivityTransitionAnimator.Controller animationController); /** + * Similar to {@link #startPendingIntentMaybeDismissingKeyguard(PendingIntent, Runnable, + * ActivityTransitionAnimator.Controller)}, but also specifies a fill-in intent and extra + * options that could be used to populate the pending intent and launch the activity. + */ + void startPendingIntentMaybeDismissingKeyguard(PendingIntent intent, + @Nullable Runnable intentSentUiThreadCallback, + @Nullable ActivityTransitionAnimator.Controller animationController, + @Nullable Intent fillInIntent, + @Nullable Bundle extraOptions); + + /** * The intent flag can be specified in startActivity(). */ void startActivity(Intent intent, boolean onlyProvisioned, boolean dismissShade, int flags); diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java index a8999ff31f8a..6c8949e51094 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java @@ -16,6 +16,7 @@ package com.android.systemui.plugins.qs; import android.content.Context; import android.view.View; +import android.view.ViewConfiguration; import android.widget.LinearLayout; import com.android.systemui.plugins.annotations.DependsOn; @@ -74,4 +75,9 @@ public abstract class QSTileView extends LinearLayout { /** Sets the index of this tile in its layout */ public abstract void setPosition(int position); + + /** Get the duration of a visuo-haptic long-press effect */ + public int getLongPressEffectDuration() { + return ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout(); + } } diff --git a/packages/SystemUI/res/drawable/biometric_prompt_vertical_list_content_view_background.xml b/packages/SystemUI/res/drawable/biometric_prompt_vertical_list_content_view_background.xml new file mode 100644 index 000000000000..fdafe6d8e335 --- /dev/null +++ b/packages/SystemUI/res/drawable/biometric_prompt_vertical_list_content_view_background.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <solid android:color="?androidprv:attr/materialColorSurfaceContainerHigh"/> + <corners android:radius="@dimen/biometric_prompt_content_corner_radius"/> +</shape> diff --git a/packages/SystemUI/res/layout-land/auth_credential_password_pin_content_view.xml b/packages/SystemUI/res/layout-land/auth_credential_password_pin_content_view.xml index 24222f7642be..1517f83814b1 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_password_pin_content_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_password_pin_content_view.xml @@ -53,6 +53,15 @@ android:layout_width="match_parent" android:layout_height="wrap_content" /> + <LinearLayout + android:id="@+id/customized_view_container" + android:orientation="vertical" + android:gravity="center_vertical" + android:layout_below="@id/subtitle" + android:layout_alignParentLeft="true" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </RelativeLayout> <FrameLayout diff --git a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml index d5af37733b3b..dd0c584d88a3 100644 --- a/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout-land/auth_credential_pattern_view.xml @@ -60,6 +60,15 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"/> + <LinearLayout + android:id="@+id/customized_view_container" + android:orientation="vertical" + android:gravity="center_vertical" + android:layout_below="@id/subtitle" + android:layout_alignParentLeft="true" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + <TextView android:id="@+id/error" style="?errorTextAppearanceLand" diff --git a/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml index 2d63c8da54f9..1777bdf92786 100644 --- a/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout-land/biometric_prompt_constraint_layout.xml @@ -2,6 +2,8 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" +xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" +android:id="@+id/biometric_prompt_constraint_layout" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -19,7 +21,7 @@ android:layout_height="match_parent"> android:id="@+id/panel" android:layout_width="0dp" android:layout_height="0dp" - android:background="?android:attr/colorBackgroundFloating" + android:background="?androidprv:attr/materialColorSurfaceBright" android:clickable="true" android:clipToOutline="true" android:importantForAccessibility="no" @@ -74,8 +76,9 @@ android:layout_height="match_parent"> <ImageView android:id="@+id/logo" - android:layout_width="@dimen/biometric_auth_icon_size" - android:layout_height="@dimen/biometric_auth_icon_size" + android:contentDescription="@string/biometric_dialog_logo" + android:layout_width="@dimen/biometric_prompt_logo_size" + android:layout_height="@dimen/biometric_prompt_logo_size" android:layout_gravity="center" android:scaleType="fitXY" android:visibility="visible" @@ -84,12 +87,9 @@ android:layout_height="match_parent"> <TextView android:id="@+id/logo_description" + style="@style/TextAppearance.AuthCredential.LogoDescription" android:layout_width="0dp" android:layout_height="wrap_content" - android:ellipsize="marquee" - android:gravity="@integer/biometric_dialog_text_gravity" - android:marqueeRepeatLimit="1" - android:singleLine="true" android:textAlignment="viewStart" android:paddingLeft="8dp" app:layout_constraintBottom_toBottomOf="@+id/logo" @@ -97,12 +97,6 @@ android:layout_height="match_parent"> app:layout_constraintStart_toEndOf="@+id/logo" app:layout_constraintTop_toTopOf="@+id/logo" /> - <Space - android:id="@+id/space_above_content" - android:layout_width="match_parent" - android:layout_height="@dimen/biometric_prompt_space_above_content" - android:visibility="gone" /> - <TextView android:id="@+id/title" style="@style/TextAppearance.AuthCredential.Title" @@ -137,11 +131,10 @@ android:layout_height="match_parent"> <LinearLayout android:id="@+id/customized_view_container" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="vertical" - android:paddingHorizontal="0dp" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -165,7 +158,6 @@ android:layout_height="match_parent"> app:layout_constraintTop_toBottomOf="@+id/subtitle" app:layout_constraintVertical_bias="0.0" /> - <androidx.constraintlayout.widget.Barrier android:id="@+id/contentBarrier" android:layout_width="wrap_content" @@ -179,16 +171,14 @@ android:layout_height="match_parent"> <TextView android:id="@+id/indicator" + style="@style/TextAppearance.AuthCredential.Indicator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:accessibilityLiveRegion="polite" android:fadingEdge="horizontal" android:gravity="center_horizontal" - android:marqueeRepeatLimit="marquee_forever" android:scrollHorizontally="true" - android:textColor="@color/biometric_dialog_gray" - android:textSize="12sp" app:layout_constraintBottom_toTopOf="@+id/buttonBarrier" app:layout_constraintEnd_toEndOf="@+id/biometric_icon" app:layout_constraintStart_toStartOf="@+id/biometric_icon" diff --git a/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml index 329fc466d378..8b886a7fdffb 100644 --- a/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout-sw600dp/biometric_prompt_constraint_layout.xml @@ -2,6 +2,8 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/biometric_prompt_constraint_layout" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -19,7 +21,7 @@ android:id="@+id/panel" android:layout_width="0dp" android:layout_height="0dp" - android:background="?android:attr/colorBackgroundFloating" + android:background="?androidprv:attr/materialColorSurfaceBright" android:clickable="true" android:clipToOutline="true" android:importantForAccessibility="no" @@ -61,8 +63,9 @@ <ImageView android:id="@+id/logo" - android:layout_width="@dimen/biometric_auth_icon_size" - android:layout_height="@dimen/biometric_auth_icon_size" + android:contentDescription="@string/biometric_dialog_logo" + android:layout_width="@dimen/biometric_prompt_logo_size" + android:layout_height="@dimen/biometric_prompt_logo_size" android:layout_gravity="center" android:scaleType="fitXY" android:visibility="visible" @@ -73,24 +76,14 @@ <TextView android:id="@+id/logo_description" + style="@style/TextAppearance.AuthCredential.LogoDescription" android:layout_width="match_parent" android:layout_height="wrap_content" - android:ellipsize="marquee" - android:gravity="@integer/biometric_dialog_text_gravity" - android:marqueeRepeatLimit="1" - android:singleLine="true" - android:paddingTop="16dp" app:layout_constraintBottom_toTopOf="@+id/title" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/logo" /> - <Space - android:id="@+id/space_above_content" - android:layout_width="match_parent" - android:layout_height="@dimen/biometric_prompt_space_above_content" - android:visibility="gone" /> - <TextView android:id="@+id/title" style="@style/TextAppearance.AuthCredential.Title" @@ -117,11 +110,10 @@ <LinearLayout android:id="@+id/customized_view_container" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" android:orientation="vertical" - android:paddingHorizontal="@dimen/biometric_prompt_content_container_padding_horizontal" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -153,16 +145,14 @@ <!-- Cancel Button, replaces negative button when biometric is accepted --> <TextView android:id="@+id/indicator" + style="@style/TextAppearance.AuthCredential.Indicator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" android:accessibilityLiveRegion="polite" android:fadingEdge="horizontal" android:gravity="center_horizontal" - android:marqueeRepeatLimit="marquee_forever" android:scrollHorizontally="true" - android:textColor="@color/biometric_dialog_gray" - android:textSize="12sp" app:layout_constraintBottom_toTopOf="@+id/buttonBarrier" app:layout_constraintEnd_toEndOf="@+id/panel" app:layout_constraintStart_toStartOf="@+id/panel" diff --git a/packages/SystemUI/res/layout/auth_credential_password_pin_content_view.xml b/packages/SystemUI/res/layout/auth_credential_password_pin_content_view.xml index 11284fd2237b..9f4fcb368a66 100644 --- a/packages/SystemUI/res/layout/auth_credential_password_pin_content_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_password_pin_content_view.xml @@ -55,6 +55,13 @@ android:layout_height="wrap_content" android:layout_below="@id/subtitle" /> + <LinearLayout + android:id="@+id/customized_view_container" + android:orientation="vertical" + android:gravity="center_vertical" + android:layout_below="@id/subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> </RelativeLayout> </ScrollView> diff --git a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml index 59828fde309f..baeb94ef2b60 100644 --- a/packages/SystemUI/res/layout/auth_credential_pattern_view.xml +++ b/packages/SystemUI/res/layout/auth_credential_pattern_view.xml @@ -56,6 +56,14 @@ android:layout_below="@id/subtitle" android:layout_width="wrap_content" android:layout_height="wrap_content"/> + + <LinearLayout + android:id="@+id/customized_view_container" + android:orientation="vertical" + android:gravity="center_vertical" + android:layout_below="@id/subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> </RelativeLayout> <RelativeLayout diff --git a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml index 6391813754d0..74bf318465b6 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml @@ -2,6 +2,8 @@ <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/biometric_prompt_constraint_layout" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -19,7 +21,7 @@ android:id="@+id/panel" android:layout_width="0dp" android:layout_height="0dp" - android:background="?android:attr/colorBackgroundFloating" + android:background="?androidprv:attr/materialColorSurfaceBright" android:clickable="true" android:clipToOutline="true" android:importantForAccessibility="no" @@ -61,8 +63,9 @@ <ImageView android:id="@+id/logo" - android:layout_width="@dimen/biometric_auth_icon_size" - android:layout_height="@dimen/biometric_auth_icon_size" + android:contentDescription="@string/biometric_dialog_logo" + android:layout_width="@dimen/biometric_prompt_logo_size" + android:layout_height="@dimen/biometric_prompt_logo_size" android:layout_gravity="center" android:scaleType="fitXY" app:layout_constraintBottom_toTopOf="@+id/logo_description" @@ -73,21 +76,14 @@ <TextView android:id="@+id/logo_description" + style="@style/TextAppearance.AuthCredential.LogoDescription" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="@integer/biometric_dialog_text_gravity" - android:singleLine="true" app:layout_constraintBottom_toTopOf="@+id/title" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/logo" /> - <Space - android:id="@+id/space_above_content" - android:layout_width="match_parent" - android:layout_height="@dimen/biometric_prompt_space_above_content" - android:visibility="gone" /> - <TextView android:id="@+id/title" style="@style/TextAppearance.AuthCredential.Title" @@ -119,7 +115,7 @@ android:gravity="center_vertical" android:orientation="vertical" android:visibility="gone" - android:paddingTop="8dp" + android:paddingTop="24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -131,7 +127,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="@integer/biometric_dialog_text_gravity" - android:paddingTop="16dp" + android:paddingTop="24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -150,6 +146,7 @@ <TextView android:id="@+id/indicator" + style="@style/TextAppearance.AuthCredential.Indicator" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="24dp" @@ -229,5 +226,4 @@ app:layout_constraintEnd_toEndOf="@+id/biometric_icon" app:layout_constraintStart_toStartOf="@+id/biometric_icon" app:layout_constraintTop_toTopOf="@+id/biometric_icon" /> - </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml b/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml index e39f60f349bc..bc827081292e 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_content_row_item_text_view.xml @@ -17,5 +17,5 @@ <TextView xmlns:android="http://schemas.android.com/apk/res/android" style="@style/TextAppearance.AuthCredential.ContentViewListItem" android:layout_width="0dp" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:layout_weight="1.0" />
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml index 6c867365e92c..f0125b60c6d8 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_content_row_layout.xml @@ -16,6 +16,6 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" - android:layout_height="@dimen/biometric_prompt_content_list_row_height" + android:layout_height="wrap_content" android:gravity="center_vertical|start" android:orientation="horizontal" /> diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_with_button_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_content_with_button_layout.xml new file mode 100644 index 000000000000..81f4bcc0bafc --- /dev/null +++ b/packages/SystemUI/res/layout/biometric_prompt_content_with_button_layout.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/customized_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/AuthCredentialContentViewStyle"> + + <TextView + android:id="@+id/customized_view_description" + style="@style/TextAppearance.AuthCredential.ContentViewWithButtonDescription" + android:paddingBottom="16dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <Button + android:id="@+id/customized_view_more_options_button" + style="@style/AuthCredentialContentViewMoreOptionsButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/biometric_dialog_content_view_more_options_button"/> +</LinearLayout> diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml index 984210906e68..ff89ed9e6e7a 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml @@ -44,7 +44,7 @@ android:singleLine="true" android:marqueeRepeatLimit="1" android:ellipsize="marquee" - style="@style/TextAppearance.AuthCredential.Title"/> + style="@style/TextAppearance.AuthCredential.OldTitle"/> <TextView android:id="@+id/subtitle" @@ -54,20 +54,21 @@ android:singleLine="true" android:marqueeRepeatLimit="1" android:ellipsize="marquee" - style="@style/TextAppearance.AuthCredential.Subtitle"/> + style="@style/TextAppearance.AuthCredential.OldSubtitle"/> <TextView android:id="@+id/description" android:layout_width="match_parent" android:layout_height="wrap_content" + android:gravity="@integer/biometric_dialog_text_gravity" android:scrollbars ="vertical" android:importantForAccessibility="no" - style="@style/TextAppearance.AuthCredential.Description"/> + style="@style/TextAppearance.AuthCredential.OldDescription"/> <Space android:id="@+id/space_above_content" android:layout_width="match_parent" - android:layout_height="@dimen/biometric_prompt_space_above_content" + android:layout_height="24dp" android:visibility="gone" /> <LinearLayout @@ -77,7 +78,6 @@ android:fadeScrollbars="false" android:gravity="center_vertical" android:orientation="vertical" - android:paddingHorizontal="@dimen/biometric_prompt_content_container_padding_horizontal" android:scrollbars="vertical" android:visibility="gone" /> diff --git a/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_vertical_list_content_layout.xml index 390875702cfe..a754cb43908a 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_content_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_vertical_list_content_layout.xml @@ -17,16 +17,11 @@ android:id="@+id/customized_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="center_vertical" - android:orientation="vertical" - style="@style/AuthCredentialContentLayoutStyle"> + style="@style/AuthCredentialVerticalListContentViewStyle"> <TextView - android:id="@+id/customized_view_title" - style="@style/TextAppearance.AuthCredential.ContentViewTitle" + android:id="@+id/customized_view_description" + style="@style/TextAppearance.AuthCredential.VerticalListContentViewDescription" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:ellipsize="marquee" - android:marqueeRepeatLimit="1" - android:singleLine="true" /> + android:layout_height="wrap_content" /> </LinearLayout> diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml new file mode 100644 index 000000000000..ef1a21f2fdf6 --- /dev/null +++ b/packages/SystemUI/res/layout/screenshot_shelf.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.systemui.screenshot.ui.ScreenshotShelfView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <ImageView + android:id="@+id/actions_container_background" + android:visibility="gone" + android:layout_height="0dp" + android:layout_width="0dp" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/actions_container" + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toTopOf="@id/guideline"/> + <HorizontalScrollView + android:id="@+id/actions_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" + android:paddingEnd="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:scrollbars="none" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_percent="1.0" + app:layout_constraintWidth_max="wrap" + app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> + <LinearLayout + android:id="@+id/screenshot_actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_share_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_edit_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_scroll_chip" + android:visibility="gone" /> + </LinearLayout> + </HorizontalScrollView> + <View + android:id="@+id/screenshot_preview_border" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="16dp" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="14dp" + android:elevation="8dp" + android:background="@drawable/overlay_border" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintBottom_toBottomOf="parent"/> + <ImageView + android:id="@+id/screenshot_preview" + android:layout_width="@dimen/overlay_x_scale" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" + android:layout_gravity="center" + android:elevation="8dp" + android:contentDescription="@string/screenshot_edit_description" + android:scaleType="fitEnd" + android:background="@drawable/overlay_preview_background" + android:adjustViewBounds="true" + android:clickable="true" + app:layout_constraintStart_toStartOf="@id/screenshot_preview_border" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/> + <ImageView + android:id="@+id/screenshot_badge" + android:layout_width="56dp" + android:layout_height="56dp" + android:visibility="gone" + android:elevation="9dp" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/> + <FrameLayout + android:id="@+id/screenshot_dismiss_button" + android:layout_width="@dimen/overlay_dismiss_button_tappable_size" + android:layout_height="@dimen/overlay_dismiss_button_tappable_size" + android:elevation="11dp" + android:visibility="gone" + app:layout_constraintStart_toEndOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintBottom_toTopOf="@id/screenshot_preview" + android:contentDescription="@string/screenshot_dismiss_description"> + <ImageView + android:id="@+id/screenshot_dismiss_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="@dimen/overlay_dismiss_button_margin" + android:background="@drawable/circular_background" + android:backgroundTint="?androidprv:attr/materialColorPrimary" + android:tint="?androidprv:attr/materialColorOnPrimary" + android:padding="4dp" + android:src="@drawable/ic_close"/> + </FrameLayout> + <ImageView + android:id="@+id/screenshot_scrollable_preview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="matrix" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/screenshot_preview" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + android:elevation="7dp"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="0dp" /> + + <FrameLayout + android:id="@+id/screenshot_message_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginTop="4dp" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + android:paddingHorizontal="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/guideline" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintWidth_max="450dp" + app:layout_constraintHorizontal_bias="0"> + <include layout="@layout/screenshot_work_profile_first_run" /> + <include layout="@layout/screenshot_detection_notice" /> + </FrameLayout> +</com.android.systemui.screenshot.ui.ScreenshotShelfView> diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml index efdb0a360031..704cf0b61b1b 100644 --- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml +++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml @@ -29,9 +29,13 @@ android:layout_height="wrap_content" android:orientation="horizontal"> <TextView + android:id="@+id/magnifier_size_title" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:singleLine="true" + android:scrollHorizontally="true" + android:ellipsize="marquee" android:text="@string/accessibility_magnifier_size" android:textAppearance="@style/TextAppearance.MagnificationSetting.Title" android:focusable="true" diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 307a6192a570..590dc682564e 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -140,9 +140,6 @@ <color name="biometric_dialog_gray">#ff757575</color> <color name="biometric_dialog_accent">@color/material_dynamic_primary40</color> <color name="biometric_dialog_error">#ffd93025</color> <!-- red 600 --> - <!-- Color for biometric prompt content view --> - <color name="biometric_prompt_content_background_color">#8AB4F8</color> - <color name="biometric_prompt_content_list_item_bullet_color">#1d873b</color> <!-- SFPS colors --> <color name="sfps_chevron_fill">@color/material_dynamic_primary90</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 2285550d94c7..e004ee9fa157 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1101,15 +1101,15 @@ <dimen name="biometric_dialog_width">240dp</dimen> <dimen name="biometric_dialog_height">240dp</dimen> - <!-- Dimensions for biometric prompt content view. --> - <dimen name="biometric_prompt_space_above_content">48dp</dimen> - <dimen name="biometric_prompt_content_container_padding_horizontal">24dp</dimen> - <dimen name="biometric_prompt_content_padding_horizontal">10dp</dimen> - <dimen name="biometric_prompt_content_list_row_height">24dp</dimen> - <dimen name="biometric_prompt_content_list_item_padding_horizontal">10dp</dimen> - <dimen name="biometric_prompt_content_list_item_text_size">14sp</dimen> - <dimen name="biometric_prompt_content_list_item_bullet_gap_width">10dp</dimen> - <dimen name="biometric_prompt_content_list_item_bullet_radius">5dp</dimen> + <!-- Dimensions for biometric prompt custom content view. --> + <dimen name="biometric_prompt_logo_size">32dp</dimen> + <dimen name="biometric_prompt_content_corner_radius">28dp</dimen> + <dimen name="biometric_prompt_content_padding_horizontal">24dp</dimen> + <dimen name="biometric_prompt_content_padding_vertical">16dp</dimen> + <dimen name="biometric_prompt_content_space_width_between_items">16dp</dimen> + <dimen name="biometric_prompt_content_list_item_padding_top">12dp</dimen> + <dimen name="biometric_prompt_content_list_item_bullet_gap_width">8.5dp</dimen> + <dimen name="biometric_prompt_content_list_item_bullet_radius">1.5dp</dimen> <!-- Biometric Auth Credential values --> <dimen name="biometric_auth_icon_size">48dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 774bbe504b03..a9151e88facc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -235,6 +235,8 @@ <string name="screenshot_edit_label">Edit</string> <!-- Content description indicating that tapping the element will allow editing the screenshot [CHAR LIMIT=NONE] --> <string name="screenshot_edit_description">Edit screenshot</string> + <!-- Label for UI element which allows sharing the screenshot [CHAR LIMIT=30] --> + <string name="screenshot_share_label">Share</string> <!-- Content description indicating that tapping the element will allow sharing the screenshot [CHAR LIMIT=NONE] --> <string name="screenshot_share_description">Share screenshot</string> <!-- Label for UI element which allows the user to capture additional off-screen content in a screenshot. [CHAR LIMIT=30] --> @@ -378,6 +380,8 @@ <!-- Button name for "Cancel". [CHAR LIMIT=NONE] --> <string name="cancel">Cancel</string> + <!-- Content description for the app logo icon on biometric prompt. [CHAR LIMIT=NONE] --> + <string name="biometric_dialog_logo">App logo</string> <!-- Message shown when a biometric is authenticated, asking the user to confirm authentication [CHAR LIMIT=30] --> <string name="biometric_dialog_confirm">Confirm</string> <!-- Button name on BiometricPrompt shown when a biometric is detected but not authenticated. Tapping the button resumes authentication [CHAR LIMIT=30] --> @@ -406,6 +410,8 @@ <string name="biometric_dialog_authenticated">Authenticated</string> <!-- Talkback string when a canceling authentication [CHAR LIMIT=NONE] --> <string name="biometric_dialog_cancel_authentication">Cancel Authentication</string> + <!-- Content description for the more options button on biometric prompt content view. [CHAR LIMIT=60] --> + <string name="biometric_dialog_content_view_more_options_button">More Options</string> <!-- Button text shown on BiometricPrompt giving the user the option to use an alternate form of authentication (Pin) [CHAR LIMIT=30] --> <string name="biometric_dialog_use_pin">Use PIN</string> @@ -1437,8 +1443,11 @@ <!-- Indication on the keyguard that appears when a trust agents unlocks the device. [CHAR LIMIT=40] --> <string name="keyguard_indication_trust_unlocked">Kept unlocked by TrustAgent</string> - <!-- Message asking the user to authenticate with primary authentication methods (PIN/pattern/password) or biometrics after the device is locked by adaptive auth. [CHAR LIMIT=60] --> - <string name="kg_prompt_after_adaptive_auth_lock">Theft protection\nDevice locked, too many unlock attempts</string> + <!-- Message asking the user to authenticate with primary authentication methods (PIN/pattern/password) or biometrics after the device is locked by adaptive auth. [CHAR LIMIT=70] --> + <string name="kg_prompt_after_adaptive_auth_lock">Device was locked, too many authentication attempts</string> + + <!-- Indication on the keyguard that appears after the device is locked by adaptive auth. [CHAR LIMIT=60] --> + <string name="keyguard_indication_after_adaptive_auth_lock">Device locked\nFailed authentication</string> <!-- Accessibility string for current zen mode and selected exit condition. A template that simply concatenates existing mode string and the current condition description. [CHAR LIMIT=20] --> <string name="zen_mode_and_condition"><xliff:g id="zen_mode" example="Priority interruptions only">%1$s</xliff:g>. <xliff:g id="exit_condition" example="For one hour">%2$s</xliff:g></string> @@ -1991,8 +2000,6 @@ <string name="group_system_cycle_back">Cycle backward through recent apps</string> <!-- User visible title for the keyboard shortcut that accesses list of all apps and search. [CHAR LIMIT=70] --> <string name="group_system_access_all_apps_search">Open apps list</string> - <!-- User visible title for the keyboard shortcut that hides and (re)showes taskbar. [CHAR LIMIT=70] --> - <string name="group_system_hide_reshow_taskbar">Show taskbar</string> <!-- User visible title for the keyboard shortcut that accesses [system] settings. [CHAR LIMIT=70] --> <string name="group_system_access_system_settings">Open settings</string> <!-- User visible title for the keyboard shortcut that accesses Assistant app. [CHAR LIMIT=70] --> @@ -2010,6 +2017,10 @@ <string name="system_multitasking_lhs">Enter split screen with current app to LHS</string> <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] --> <string name="system_multitasking_full_screen">Switch from split screen to full screen</string> + <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] --> + <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string> + <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] --> + <string name="system_multitasking_splitscreen_focus_lhs">Switch to app on left or above while using split screen</string> <!-- User visible title for the keyboard shortcut that replaces an app from one to another during split screen [CHAR LIMIT=70] --> <string name="system_multitasking_replace">During split screen: replace an app from one to another</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 59516be65a5e..455b1920706f 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -174,43 +174,61 @@ <item name="android:textColor">?android:attr/textColorPrimary</item> </style> - <style name="TextAppearance.AuthCredential.Title"> + <style name="TextAppearance.AuthCredential.OldTitle"> <item name="android:fontFamily">google-sans</item> <item name="android:paddingTop">12dp</item> <item name="android:paddingHorizontal">24dp</item> <item name="android:textSize">24sp</item> </style> - <style name="TextAppearance.AuthCredential.Subtitle"> + <style name="TextAppearance.AuthCredential.OldSubtitle"> <item name="android:fontFamily">google-sans</item> <item name="android:paddingTop">8dp</item> <item name="android:paddingHorizontal">24dp</item> <item name="android:textSize">16sp</item> </style> - <style name="TextAppearance.AuthCredential.Description"> + <style name="TextAppearance.AuthCredential.OldDescription"> <item name="android:fontFamily">google-sans</item> <item name="android:paddingTop">8dp</item> <item name="android:paddingHorizontal">24dp</item> <item name="android:textSize">14sp</item> </style> - <style name="TextAppearance.AuthCredential.ContentViewTitle"> - <item name="android:fontFamily">google-sans</item> - <item name="android:paddingTop">8dp</item> - <item name="android:paddingHorizontal">24dp</item> - <item name="android:textSize">14sp</item> - <item name="android:gravity">start</item> + <style name="TextAppearance.AuthCredential.LogoDescription" parent="TextAppearance.Material3.LabelLarge" > + <item name="android:ellipsize">marquee</item> + <item name="android:gravity">@integer/biometric_dialog_text_gravity</item> + <item name="android:marqueeRepeatLimit">1</item> + <item name="android:singleLine">true</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item> </style> - <style name="TextAppearance.AuthCredential.ContentViewListItem"> - <item name="android:fontFamily">google-sans</item> - <item name="android:paddingTop">8dp</item> - <item name="android:paddingHorizontal"> - @dimen/biometric_prompt_content_list_item_padding_horizontal - </item> - <item name="android:textSize">@dimen/biometric_prompt_content_list_item_text_size</item> - <item name="android:gravity">start</item> + <style name="TextAppearance.AuthCredential.Title" parent="TextAppearance.Material3.HeadlineSmall" > + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + </style> + + <style name="TextAppearance.AuthCredential.Subtitle" parent="TextAppearance.Material3.BodyMedium" > + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + </style> + + <style name="TextAppearance.AuthCredential.Description" parent="TextAppearance.Material3.BodyMedium" > + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + </style> + + <style name="TextAppearance.AuthCredential.VerticalListContentViewDescription" parent="TextAppearance.Material3.TitleSmall"> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + </style> + + <style name="TextAppearance.AuthCredential.ContentViewWithButtonDescription" parent="TextAppearance.AuthCredential.Description" /> + + <style name="TextAppearance.AuthCredential.ContentViewListItem" parent="TextAppearance.Material3.BodySmall"> + <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item> + <item name="android:paddingTop">@dimen/biometric_prompt_content_list_item_padding_top</item> + </style> + + <style name="TextAppearance.AuthCredential.Indicator" parent="TextAppearance.Material3.BodyMedium"> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + <item name="android:marqueeRepeatLimit">marquee_forever</item> </style> <style name="TextAppearance.AuthCredential.Error"> @@ -312,9 +330,27 @@ <item name="android:textSize">16sp</item> </style> - <style name="AuthCredentialContentLayoutStyle"> - <item name="android:background">@color/biometric_prompt_content_background_color</item> + <style name="AuthCredentialContentViewStyle"> + <item name="android:gravity">center_vertical</item> + <item name="android:orientation">vertical</item> + </style> + + <style name="AuthCredentialVerticalListContentViewStyle" parent="AuthCredentialContentViewStyle"> + <item name="android:background">@drawable/biometric_prompt_vertical_list_content_view_background</item> <item name="android:paddingHorizontal">@dimen/biometric_prompt_content_padding_horizontal</item> + <item name="android:paddingVertical">@dimen/biometric_prompt_content_padding_vertical</item> + </style> + + <style name="AuthCredentialContentViewMoreOptionsButtonStyle" parent="TextAppearance.Material3.LabelLarge"> + <item name="android:background">@color/transparent</item> + <item name="android:gravity">start</item> + <item name="enforceTextAppearance">false</item> + <item name="android:height">40dp</item> + <item name="android:maxWidth">@dimen/m3_btn_max_width</item> + <item name="android:minWidth">48dp</item> + <item name="android:paddingLeft">0dp</item> + <item name="android:paddingRight">12dp</item> + <item name="android:textColor">?androidprv:attr/materialColorPrimary</item> </style> <style name="DeviceManagementDialogTitle"> @@ -962,7 +998,7 @@ <item name="android:windowBackground">@android:color/transparent</item> <item name="android:backgroundDimEnabled">true</item> <item name="android:windowCloseOnTouchOutside">true</item> - <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item> + <item name="android:windowAnimationStyle">@null</item> </style> <style name="Widget.SliceView.VolumePanel"> diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 8a2245d3d14c..48271dea31d8 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -31,7 +31,6 @@ import android.view.ViewTreeObserver.OnGlobalLayoutListener import androidx.annotation.VisibleForTesting import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.customization.R import com.android.systemui.dagger.qualifiers.Background @@ -39,6 +38,7 @@ import com.android.systemui.dagger.qualifiers.DisplaySpecific import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags.REGION_SAMPLING +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.AOD @@ -328,7 +328,7 @@ constructor( object : KeyguardUpdateMonitorCallback() { override fun onKeyguardVisibilityChanged(visible: Boolean) { isKeyguardVisible = visible - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { if (!isKeyguardVisible) { clock?.run { smallClock.animations.doze(if (isDozing) 1f else 0f) @@ -368,7 +368,7 @@ constructor( } private fun refreshTime() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } @@ -427,7 +427,7 @@ constructor( parent.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { listenForDozing(this) - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { listenForDozeAmountTransition(this) listenForAnyStateToAodTransition(this) } else { diff --git a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt index 630610d1a85f..df77a58c3b34 100644 --- a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt +++ b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt @@ -30,7 +30,7 @@ import android.view.WindowManager import android.widget.FrameLayout import android.widget.FrameLayout.LayoutParams import com.android.keyguard.dagger.KeyguardStatusViewComponent -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.res.R @@ -95,7 +95,7 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { onCreateV2() } else { onCreate() @@ -132,7 +132,7 @@ constructor( } override fun onAttachedToWindow() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { clockRegistry.registerClockChangeListener(clockChangedListener) clockEventController.registerListeners(clock!!) @@ -141,7 +141,7 @@ constructor( } override fun onDetachedFromWindow() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { clockEventController.unregisterListeners() clockRegistry.unregisterClockChangeListener(clockChangedListener) } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 28013c6c8289..4a96e9e0845a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -3,7 +3,6 @@ package com.android.keyguard; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -23,6 +22,7 @@ import androidx.core.content.res.ResourcesCompat; import com.android.app.animation.Interpolators; import com.android.keyguard.dagger.KeyguardStatusViewScope; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.LogLevel; import com.android.systemui.plugins.clocks.ClockController; @@ -192,7 +192,7 @@ public class KeyguardClockSwitch extends RelativeLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); mStatusArea = findViewById(R.id.keyguard_status_area); @@ -266,7 +266,7 @@ public class KeyguardClockSwitch extends RelativeLayout { } void updateClockTargetRegions() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } if (mClock != null) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index e621ffe4cbc4..5b8eb9d3da82 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -21,7 +21,6 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.Flags.smartspaceRelocateToBottom; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; @@ -45,6 +44,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; @@ -202,7 +202,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mClockChangedListener = new ClockRegistry.ClockChangeListener() { @Override public void onCurrentClockChanged() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { setClock(mClockRegistry.createCurrentClock()); } } @@ -245,7 +245,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS protected void onInit() { mKeyguardSliceViewController.init(); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view); mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large); } @@ -340,7 +340,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS addDateWeatherView(); } } - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { setDateWeatherVisibility(); setWeatherVisibility(); } @@ -348,7 +348,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } int getNotificationIconAreaHeight() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return 0; } else if (NotificationIconContainerRefactor.isEnabled()) { return mAodIconContainer != null ? mAodIconContainer.getHeight() : 0; @@ -391,7 +391,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void addDateWeatherView() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } mDateWeatherView = (ViewGroup) mSmartspaceController.buildAndConnectDateView(mView); @@ -407,7 +407,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void addWeatherView() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( @@ -420,7 +420,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void addSmartspaceView() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } @@ -528,7 +528,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS */ void updatePosition(int x, float scale, AnimationProperties props, boolean animate) { x = getCurrentLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -x : x; - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { PropertyAnimator.setProperty(mSmallClockFrame, AnimatableProperty.TRANSLATION_X, x, props, animate); PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_X, @@ -554,7 +554,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS return 0; } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return 0; } @@ -589,14 +589,14 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } boolean isClockTopAligned() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return mKeyguardClockInteractor.getClockSize().getValue() == LARGE; } return mLargeClockFrame.getVisibility() != View.VISIBLE; } private void updateAodIcons() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { NotificationIconContainer nic = (NotificationIconContainer) mView.findViewById( com.android.systemui.res.R.id.left_aligned_notification_icon_container); @@ -616,7 +616,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void setClock(ClockController clock) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } if (clock != null && mLogBuffer != null) { @@ -630,8 +630,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS @Nullable public ClockController getClock() { - if (migrateClocksToBlueprint()) { - return mKeyguardClockInteractor.getClock(); + if (MigrateClocksToBlueprint.isEnabled()) { + return mKeyguardClockInteractor.getCurrentClock().getValue(); } else { return mClockEventController.getClock(); } @@ -642,7 +642,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void updateDoubleLineClock() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } mCanShowDoubleLineClock = mSecureSettings.getIntForUser( diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index 7f9ae5e578e6..603a47e8d26e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -20,7 +20,6 @@ import static androidx.constraintlayout.widget.ConstraintSet.END; import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.animation.Animator; @@ -52,6 +51,7 @@ import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.Dumpable; import com.android.systemui.animation.ViewHierarchyAnimator; import com.android.systemui.dump.DumpManager; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.plugins.clocks.ClockController; import com.android.systemui.power.domain.interactor.PowerInteractor; @@ -223,7 +223,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV } mDumpManager.registerDumpable(getInstanceName(), this); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { startCoroutines(EmptyCoroutineContext.INSTANCE); mView.setVisibility(View.GONE); } @@ -250,7 +250,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV @Override protected void onViewAttached() { mStatusArea = mView.findViewById(R.id.keyguard_status_area); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } @@ -261,7 +261,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV @Override protected void onViewDetached() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } @@ -485,7 +485,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV boolean splitShadeEnabled, boolean shouldBeCentered, boolean animate) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered); } else { mKeyguardClockSwitchController.setSplitShadeCentered( @@ -503,7 +503,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(layout); int guideline; - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { guideline = R.id.split_shade_guideline; } else { guideline = R.id.qs_edge_guideline; @@ -548,7 +548,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV && clock.getLargeClock().getConfig().getHasCustomPositionUpdatedAnimation(); // When migrateClocksToBlueprint is on, customized clock animation is conducted in // KeyguardClockViewBinder - if (customClockAnimation && !migrateClocksToBlueprint()) { + if (customClockAnimation && !MigrateClocksToBlueprint.isEnabled()) { // Find the clock, so we can exclude it from this transition. FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java index c3c42399f1f7..9b09265763a2 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java @@ -24,7 +24,7 @@ import androidx.annotation.Nullable; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.shade.ShadeExpansionStateManager; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.statusbar.phone.BiometricUnlockController; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.KeyguardBypassController; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java index f5a6cb35b545..fd8b6d5f05e1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java @@ -16,7 +16,6 @@ package com.android.keyguard; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; @@ -24,6 +23,7 @@ import android.util.Property; import android.view.View; import com.android.app.animation.Interpolators; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.LogLevel; import com.android.systemui.statusbar.StatusBarState; @@ -88,7 +88,7 @@ public class KeyguardVisibilityHelper { boolean keyguardFadingAway, boolean goingToFullShade, int oldStatusBarState) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { log("Ignoring KeyguardVisibilityelper, migrateClocksToBlueprint flag on"); return; } @@ -113,7 +113,7 @@ public class KeyguardVisibilityHelper { animProps.setDelay(0).setDuration(160); log("goingToFullShade && !keyguardFadingAway"); } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { log("Using LockscreenToGoneTransition 1"); } else { PropertyAnimator.setProperty( @@ -171,7 +171,7 @@ public class KeyguardVisibilityHelper { animProps, true /* animate */); } else if (mScreenOffAnimationController.shouldAnimateInKeyguard()) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { log("Using GoneToAodTransition"); mKeyguardViewVisibilityAnimating = false; } else { @@ -187,7 +187,7 @@ public class KeyguardVisibilityHelper { mView.setVisibility(View.VISIBLE); } } else { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { log("Using LockscreenToGoneTransition 2"); } else { log("Direct set Visibility to GONE"); diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index 039a2e5a8ffc..8f1a5f79687c 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -22,8 +22,6 @@ import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT; import static com.android.keyguard.LockIconView.ICON_FINGERPRINT; import static com.android.keyguard.LockIconView.ICON_LOCK; import static com.android.keyguard.LockIconView.ICON_UNLOCK; -import static com.android.systemui.Flags.keyguardBottomAreaRefactor; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; @@ -68,6 +66,8 @@ import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionStep; @@ -453,7 +453,7 @@ public class LockIconViewController implements Dumpable { private void updateLockIconLocation() { final float scaleFactor = mAuthController.getScaleFactor(); final int scaledPadding = (int) (mDefaultPaddingPx * scaleFactor); - if (keyguardBottomAreaRefactor() || migrateClocksToBlueprint()) { + if (KeyguardBottomAreaRefactor.isEnabled() || MigrateClocksToBlueprint.isEnabled()) { mView.getLockIcon().setPadding(scaledPadding, scaledPadding, scaledPadding, scaledPadding); } else { diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index a0f15efe7025..781f6dda18e8 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -16,8 +16,6 @@ package com.android.keyguard.dagger; -import static com.android.systemui.Flags.migrateClocksToBlueprint; - import android.content.Context; import android.content.res.Resources; import android.view.LayoutInflater; @@ -28,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.clocks.ClockMessageBuffers; import com.android.systemui.res.R; @@ -70,7 +69,7 @@ public abstract class ClockRegistryModule { layoutInflater, resources, featureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION), - migrateClocksToBlueprint()), + MigrateClocksToBlueprint.isEnabled()), context.getString(R.string.lockscreen_clock_id_fallback), clockBuffers, /* keepAllLoaded = */ false, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index a98990af00c7..ca24ccb3e6ec 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -98,6 +98,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private ImageButton mMediumButton; private ImageButton mLargeButton; private Button mDoneButton; + private TextView mSizeTitle; private Button mEditButton; private ImageButton mFullScreenButton; private int mLastSelectedButtonIndex = MagnificationSize.NONE; @@ -521,6 +522,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mMediumButton = mSettingView.findViewById(R.id.magnifier_medium_button); mLargeButton = mSettingView.findViewById(R.id.magnifier_large_button); mDoneButton = mSettingView.findViewById(R.id.magnifier_done_button); + mSizeTitle = mSettingView.findViewById(R.id.magnifier_size_title); mEditButton = mSettingView.findViewById(R.id.magnifier_edit_button); mFullScreenButton = mSettingView.findViewById(R.id.magnifier_full_button); mAllowDiagonalScrollingTitle = @@ -548,6 +550,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mDoneButton.setOnClickListener(mButtonClickListener); mFullScreenButton.setOnClickListener(mButtonClickListener); mEditButton.setOnClickListener(mButtonClickListener); + mSizeTitle.setSelected(true); mAllowDiagonalScrollingTitle.setSelected(true); mSettingView.setOnApplyWindowInsetsListener((v, insets) -> { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 99cdc0181553..fd0e7fc04ef3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -218,6 +218,11 @@ public class AuthContainerView extends LinearLayout } @Override + public void onContentViewMoreOptionsButtonPressed() { + animateAway(AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS); + } + + @Override public void onError() { animateAway(AuthDialogCallback.DISMISSED_ERROR); } @@ -513,7 +518,8 @@ public class AuthContainerView extends LinearLayout mConfig.mOpPackageName); final CredentialViewModel vm = mCredentialViewModelProvider.get(); vm.setAnimateContents(animateContents); - ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel); + ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel, + mBiometricCallback); mLayout.addView(mCredentialView); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index a40b4d733382..d85b81d4d953 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -205,12 +205,12 @@ public class AuthController implements if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { String reason = intent.getStringExtra("reason"); reason = (reason != null) ? reason : "unknown"; - closeDioalog(reason); + closeDialog(reason); } } }; - private void closeDioalog(String reason) { + private void closeDialog(String reason) { if (isShowing()) { Log.i(TAG, "Close BP, reason :" + reason); mCurrentDialog.dismissWithoutCallback(true /* animate */); @@ -571,6 +571,11 @@ public class AuthController implements credentialAttestation); break; + case AuthDialogCallback.DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS: + sendResultAndCleanUp( + BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS, + credentialAttestation); + break; default: Log.e(TAG, "Unhandled reason: " + reason); break; @@ -579,7 +584,7 @@ public class AuthController implements @Override public void handleShowGlobalActionsMenu() { - closeDioalog("PowerMenu shown"); + closeDialog("PowerMenu shown"); } /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java index 9a2194025a1a..024c6eaa75bb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogCallback.java @@ -32,6 +32,7 @@ public interface AuthDialogCallback { int DISMISSED_ERROR = 5; int DISMISSED_BY_SYSTEM_SERVER = 6; int DISMISSED_CREDENTIAL_AUTHENTICATED = 7; + int DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS = 8; @IntDef({DISMISSED_USER_CANCELED, DISMISSED_BUTTON_NEGATIVE, @@ -39,7 +40,8 @@ public interface AuthDialogCallback { DISMISSED_BIOMETRIC_AUTHENTICATED, DISMISSED_ERROR, DISMISSED_BY_SYSTEM_SERVER, - DISMISSED_CREDENTIAL_AUTHENTICATED}) + DISMISSED_CREDENTIAL_AUTHENTICATED, + DISMISSED_BUTTON_CONTENT_VIEW_MORE_OPTIONS}) @interface DismissedReason {} /** diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt index ac99fc69b2b5..85f63e9f1974 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt @@ -126,6 +126,7 @@ constructor( field?.let { oldView -> val lottie = oldView.requireViewById(R.id.sidefps_animation) as LottieAnimationView lottie.pauseAnimation() + lottie.removeAllLottieOnCompositionLoadedListener() windowManager.removeView(oldView) orientationListener.disable() } @@ -288,7 +289,7 @@ constructor( } private fun onOrientationChanged(@BiometricRequestConstants.RequestReason reason: Int) { - if (overlayView != null) { + if (overlayView?.isAttachedToWindow == true) { createOverlayForDisplay(reason) } } @@ -322,7 +323,7 @@ constructor( ) lottie.addLottieOnCompositionLoadedListener { // Check that view is not stale, and that overlayView has not been hidden/removed - if (overlayView != null && overlayView == view) { + if (overlayView?.isAttachedToWindow == true && overlayView == view) { updateOverlayParams(display, it.bounds) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt index 4d88f4945952..9ad3f4313838 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -155,7 +155,8 @@ constructor( constraintBp() && !Utils.isBiometricAllowed(promptInfo) && isDeviceCredentialAllowed(promptInfo) && - promptInfo.contentView != null + promptInfo.contentView != null && + !promptInfo.isContentViewMoreOptionsButtonUsed _showBpWithoutIconForCredential.value = showBpForCredential && !hasCredentialViewShown } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt index ed1557cccd01..c4967ec0df21 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/BiometricStatusInteractor.kt @@ -23,6 +23,7 @@ import com.android.systemui.biometrics.shared.model.AuthenticationReason.Setting import com.android.systemui.keyguard.shared.model.FingerprintAuthenticationStatus import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map /** Encapsulates business logic for interacting with biometric authentication state. */ @@ -52,7 +53,7 @@ constructor( } else { AuthenticationReason.NotRunning } - } + }.distinctUntilChanged() override val fingerprintAcquiredStatus: Flow<FingerprintAuthenticationStatus> = biometricStatusRepository.fingerprintAcquiredStatus diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt index 94cea5702fe3..b7c0fa802db3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt @@ -30,11 +30,11 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.lastOrNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext @@ -61,11 +61,12 @@ constructor( val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing /** - * If biometric prompt without icon needs to show for displaying content prior to credential - * view. + * If vertical list content view is shown, credential view should hide subtitle and content view */ - val showBpWithoutIconForCredential: StateFlow<Boolean> = - biometricPromptRepository.showBpWithoutIconForCredential + val showTitleOnly: Flow<Boolean> = + biometricPromptRepository.promptInfo.map { promptInfo -> + promptInfo?.contentView != null && !promptInfo.isContentViewMoreOptionsButtonUsed + } /** Metadata about the current credential prompt, including app-supplied preferences. */ val prompt: Flow<BiometricPromptRequest.Credential?> = diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt index 2f493ac1dccf..b28733f5cc55 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt @@ -10,10 +10,11 @@ import android.view.WindowInsets import android.view.accessibility.AccessibilityManager import android.widget.LinearLayout import android.widget.TextView -import com.android.systemui.res.R import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.ui.binder.CredentialViewBinder +import com.android.systemui.biometrics.ui.binder.Spaghetti import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel +import com.android.systemui.res.R /** PIN or password credential view for BiometricPrompt. */ class CredentialPasswordView(context: Context, attrs: AttributeSet?) : @@ -31,8 +32,16 @@ class CredentialPasswordView(context: Context, attrs: AttributeSet?) : host: CredentialView.Host, panelViewController: AuthPanelController, animatePanel: Boolean, + legacyCallback: Spaghetti.Callback, ) { - CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel) + CredentialViewBinder.bind( + this, + host, + viewModel, + panelViewController, + animatePanel, + legacyCallback + ) } override fun onFinishInflate() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt index 10868970fcbb..d9d286fe7035 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt @@ -9,6 +9,7 @@ import android.view.WindowInsets.Type import android.widget.LinearLayout import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.ui.binder.CredentialViewBinder +import com.android.systemui.biometrics.ui.binder.Spaghetti import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel /** Pattern credential view for BiometricPrompt. */ @@ -21,8 +22,16 @@ class CredentialPatternView(context: Context, attrs: AttributeSet?) : host: CredentialView.Host, panelViewController: AuthPanelController, animatePanel: Boolean, + legacyCallback: Spaghetti.Callback, ) { - CredentialViewBinder.bind(this, host, viewModel, panelViewController, animatePanel) + CredentialViewBinder.bind( + this, + host, + viewModel, + panelViewController, + animatePanel, + legacyCallback + ) } override fun onFinishInflate() { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt index b7c6a4566108..e2f98958ab55 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt @@ -1,6 +1,7 @@ package com.android.systemui.biometrics.ui import com.android.systemui.biometrics.AuthPanelController +import com.android.systemui.biometrics.ui.binder.Spaghetti import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel /** A credential variant of BiometricPrompt. */ @@ -27,5 +28,6 @@ sealed interface CredentialView { host: Host, panelViewController: AuthPanelController, animatePanel: Boolean, + legacyCallback: Spaghetti.Callback, ) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt index e58c8ff92c03..88aef5675240 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt @@ -18,101 +18,167 @@ package com.android.systemui.biometrics.ui.binder import android.content.Context import android.content.res.Resources -import android.content.res.Resources.Theme -import android.graphics.Paint import android.hardware.biometrics.PromptContentItem import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptContentItemPlainText import android.hardware.biometrics.PromptContentView +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptVerticalListContentView import android.text.SpannableString import android.text.Spanned +import android.text.TextPaint import android.text.style.BulletSpan import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.view.ViewTreeObserver +import android.widget.Button import android.widget.LinearLayout import android.widget.Space import android.widget.TextView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.lifecycleScope +import com.android.settingslib.Utils import com.android.systemui.biometrics.ui.BiometricPromptLayout -import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R import kotlin.math.ceil -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch /** Sub-binder for [BiometricPromptLayout.customized_view_container]. */ object BiometricCustomizedViewBinder { - fun bind(customizedViewContainer: LinearLayout, spaceAbove: Space, viewModel: PromptViewModel) { - customizedViewContainer.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - val contentView: PromptContentView? = viewModel.contentView.first() - - if (contentView != null) { - val context = customizedViewContainer.context - customizedViewContainer.addView(contentView.toView(context)) - customizedViewContainer.visibility = View.VISIBLE - spaceAbove.visibility = View.VISIBLE - } else { - customizedViewContainer.visibility = View.GONE - spaceAbove.visibility = View.GONE + fun bind( + customizedViewContainer: LinearLayout, + contentView: PromptContentView?, + legacyCallback: Spaghetti.Callback + ) { + customizedViewContainer.repeatWhenAttached { containerView -> + lifecycleScope.launch { + if (contentView == null) { + containerView.visibility = View.GONE + return@launch + } + + containerView.width { containerWidth -> + if (containerWidth == 0) { + return@width } + (containerView as LinearLayout).addView( + contentView.toView(containerView.context, containerWidth, legacyCallback) + ) + containerView.visibility = View.VISIBLE } } } } } -private fun PromptContentView.toView(context: Context): View { - val resources = context.resources +private fun PromptContentView.toView( + context: Context, + containerViewWidth: Int, + legacyCallback: Spaghetti.Callback +): View { + return when (this) { + is PromptVerticalListContentView -> initLayout(context, containerViewWidth) + is PromptContentViewWithMoreOptionsButton -> initLayout(context, legacyCallback) + else -> { + throw IllegalStateException("No such PromptContentView: $this") + } + } +} + +private fun LayoutInflater.inflateContentView(id: Int, description: String?): LinearLayout { + val contentView = inflate(id, null) as LinearLayout + + val descriptionView = contentView.requireViewById<TextView>(R.id.customized_view_description) + if (!description.isNullOrEmpty()) { + descriptionView.text = description + } else { + descriptionView.visibility = View.GONE + } + return contentView +} + +private fun PromptContentViewWithMoreOptionsButton.initLayout( + context: Context, + legacyCallback: Spaghetti.Callback +): View { val inflater = LayoutInflater.from(context) - when (this) { - is PromptVerticalListContentView -> { - val contentView = - inflater.inflate(R.layout.biometric_prompt_content_layout, null) as LinearLayout - - val descriptionView = contentView.requireViewById<TextView>(R.id.customized_view_title) - if (!description.isNullOrEmpty()) { - descriptionView.text = description - } else { - descriptionView.visibility = View.GONE - } + val contentView = + inflater.inflateContentView( + R.layout.biometric_prompt_content_with_button_layout, + description + ) + val buttonView = contentView.requireViewById<Button>(R.id.customized_view_more_options_button) + buttonView.setOnClickListener { legacyCallback.onContentViewMoreOptionsButtonPressed() } + return contentView +} - // Show two column by default, once there is an item exceeding max lines, show single - // item instead. - val showTwoColumn = listItems.all { !it.doesExceedMaxLinesIfTwoColumn(resources) } - var currRowView = createNewRowLayout(inflater) - for (item in listItems) { - val itemView = item.toView(resources, inflater, context.theme) - currRowView.addView(itemView) - - if (!showTwoColumn || currRowView.childCount == 2) { - contentView.addView(currRowView) - currRowView = createNewRowLayout(inflater) - } - } - if (currRowView.childCount > 0) { - contentView.addView(currRowView) - } +private fun PromptVerticalListContentView.initLayout( + context: Context, + containerViewWidth: Int +): View { + val inflater = LayoutInflater.from(context) + val resources = context.resources + val contentView = + inflater.inflateContentView( + R.layout.biometric_prompt_vertical_list_content_layout, + description + ) + // Show two column by default, once there is an item exceeding max lines, show single + // item instead. + val showTwoColumn = + listItems.all { !it.doesExceedMaxLinesIfTwoColumn(context, containerViewWidth) } + var currRowView = createNewRowLayout(inflater) + for (item in listItems) { + val itemView = item.toView(context, inflater) + // If this item will be in the first row (contentView only has description view) and + // description is empty, remove top padding of this item. + if (contentView.childCount == 1 && description.isNullOrEmpty()) { + itemView.setPadding( + itemView.paddingLeft, + 0, + itemView.paddingRight, + itemView.paddingBottom + ) + } + currRowView.addView(itemView) - return contentView + // If this is the first item in the current row, add space behind it. + if (currRowView.childCount == 1 && showTwoColumn) { + currRowView.addSpaceView( + resources.getDimensionPixelSize( + R.dimen.biometric_prompt_content_space_width_between_items + ), + MATCH_PARENT + ) } - else -> { - throw IllegalStateException("No such PromptContentView: $this") + + // If there are already two items (plus the space view) in the current row, or it + // should be one column, start a new row + if (currRowView.childCount == 3 || !showTwoColumn) { + contentView.addView(currRowView) + currRowView = createNewRowLayout(inflater) } } + if (currRowView.childCount > 0) { + contentView.addView(currRowView) + } + return contentView } private fun createNewRowLayout(inflater: LayoutInflater): LinearLayout { return inflater.inflate(R.layout.biometric_prompt_content_row_layout, null) as LinearLayout } +private fun LinearLayout.addSpaceView(width: Int, height: Int) { + addView(Space(context), LinearLayout.LayoutParams(width, height)) +} + private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( - resources: Resources, + context: Context, + containerViewWidth: Int, ): Boolean { + val resources = context.resources val passedInText: String = when (this) { is PromptContentItemPlainText -> text @@ -125,32 +191,26 @@ private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( when (this) { is PromptContentItemPlainText, is PromptContentItemBulletedText -> { - val dialogMargin = - resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding) - val halfDialogWidth = - Resources.getSystem().displayMetrics.widthPixels / 2 - dialogMargin - val containerPadding = - resources.getDimensionPixelSize( - R.dimen.biometric_prompt_content_container_padding_horizontal - ) - val contentPadding = + val contentViewPadding = resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_padding_horizontal) val listItemPadding = getListItemPadding(resources) - val maxWidth = halfDialogWidth - containerPadding - contentPadding - listItemPadding + val maxWidth = containerViewWidth / 2 - contentViewPadding - listItemPadding - val text = "$passedInText" - val textSize = - resources.getDimensionPixelSize( - R.dimen.biometric_prompt_content_list_item_text_size + val paint = TextPaint() + val attributes = + context.obtainStyledAttributes( + R.style.TextAppearance_AuthCredential_ContentViewListItem, + intArrayOf(android.R.attr.textSize) ) - val paint = Paint() - paint.textSize = textSize.toFloat() + paint.textSize = attributes.getDimensionPixelSize(0, 0).toFloat() + val textWidth = paint.measureText(passedInText) + attributes.recycle() val maxLines = resources.getInteger( R.integer.biometric_prompt_content_list_item_max_lines_if_two_column ) - val numLines = ceil(paint.measureText(text).toDouble() / maxWidth).toInt() + val numLines = ceil(textWidth / maxWidth).toInt() return numLines > maxLines } else -> { @@ -160,10 +220,10 @@ private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( } private fun PromptContentItem.toView( - resources: Resources, + context: Context, inflater: LayoutInflater, - theme: Theme, ): TextView { + val resources = context.resources val textView = inflater.inflate(R.layout.biometric_prompt_content_row_item_text_view, null) as TextView val lp = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1f) @@ -178,7 +238,7 @@ private fun PromptContentItem.toView( val span = BulletSpan( getListItemBulletGapWidth(resources), - getListItemBulletColor(resources, theme), + getListItemBulletColor(context), getListItemBulletRadius(resources) ) bulletedText.setSpan(span, 0 /* start */, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) @@ -194,8 +254,8 @@ private fun PromptContentItem.toView( private fun PromptContentItem.getListItemPadding(resources: Resources): Int { var listItemPadding = resources.getDimensionPixelSize( - R.dimen.biometric_prompt_content_list_item_padding_horizontal - ) * 2 + R.dimen.biometric_prompt_content_space_width_between_items + ) / 2 when (this) { is PromptContentItemPlainText -> {} is PromptContentItemBulletedText -> { @@ -215,5 +275,20 @@ private fun getListItemBulletRadius(resources: Resources): Int = private fun getListItemBulletGapWidth(resources: Resources): Int = resources.getDimensionPixelSize(R.dimen.biometric_prompt_content_list_item_bullet_gap_width) -private fun getListItemBulletColor(resources: Resources, theme: Theme): Int = - resources.getColor(R.color.biometric_prompt_content_list_item_bullet_color, theme) +private fun getListItemBulletColor(context: Context): Int = + Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.materialColorOnSurface) + +private fun <T : View> T.width(function: (Int) -> Unit) { + if (width == 0) + viewTreeObserver.addOnGlobalLayoutListener( + object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + if (measuredWidth > 0) { + viewTreeObserver.removeOnGlobalLayoutListener(this) + } + function(measuredWidth) + } + } + ) + else function(measuredWidth) +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 7bb75bf5ca9b..b2ade4fa1e8a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -135,7 +135,7 @@ object BiometricViewBinder { val confirmationButton = view.requireViewById<Button>(R.id.button_confirm) val retryButton = view.requireViewById<Button>(R.id.button_try_again) - // TODO(b/251476085): temporary workaround for the unsafe callbacks & legacy controllers + // TODO(b/330788871): temporary workaround for the unsafe callbacks & legacy controllers val adapter = Spaghetti( view = view, @@ -171,8 +171,8 @@ object BiometricViewBinder { if (Flags.customBiometricPrompt() && constraintBp()) { BiometricCustomizedViewBinder.bind( customizedViewContainer, - view.requireViewById(R.id.space_above_content), - viewModel + viewModel.contentView.first(), + legacyCallback ) } @@ -476,7 +476,7 @@ object BiometricViewBinder { * * Do not reference the [view] for anything other than [asView]. */ -@Deprecated("TODO(b/251476085): remove after replacing AuthContainerView") +@Deprecated("TODO(b/330788871): remove after replacing AuthContainerView") class Spaghetti( private val view: View, private val viewModel: PromptViewModel, @@ -484,19 +484,20 @@ class Spaghetti( private val applicationScope: CoroutineScope, ) { - @Deprecated("TODO(b/251476085): remove after replacing AuthContainerView") + @Deprecated("TODO(b/330788871): remove after replacing AuthContainerView") interface Callback { fun onAuthenticated() fun onUserCanceled() fun onButtonNegative() fun onButtonTryAgain() + fun onContentViewMoreOptionsButtonPressed() fun onError() fun onUseDeviceCredential() fun onStartDelayedFingerprintSensor() fun onAuthenticatedAndConfirmed() } - @Deprecated("TODO(b/251476085): remove after replacing AuthContainerView") + @Deprecated("TODO(b/330788871): remove after replacing AuthContainerView") enum class BiometricState { /** Authentication hardware idle. */ STATE_IDLE, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index 1dfd2e5f9cc9..e3c0cba42e2d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -121,10 +121,6 @@ object BiometricViewSizeBinder { val largeConstraintSet = ConstraintSet() largeConstraintSet.clone(mediumConstraintSet) - largeConstraintSet.setVisibility(iconHolderView.id, View.GONE) - largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE) - largeConstraintSet.setVisibility(R.id.indicator, View.GONE) - largeConstraintSet.setVisibility(R.id.scrollView, View.GONE) // TODO: Investigate better way to handle 180 rotations val flipConstraintSet = ConstraintSet() @@ -286,6 +282,10 @@ object BiometricViewSizeBinder { fun setVisibilities(size: PromptSize) { viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) } + largeConstraintSet.setVisibility(iconHolderView.id, View.GONE) + largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE) + largeConstraintSet.setVisibility(R.id.indicator, View.GONE) + largeConstraintSet.setVisibility(R.id.scrollView, View.GONE) if (viewModel.showBpWithoutIconForCredential.value) { smallConstraintSet.setVisibility(iconHolderView.id, View.GONE) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt index ce52e1d78fda..18e2a56e5e78 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt @@ -1,20 +1,23 @@ package com.android.systemui.biometrics.ui.binder +import android.hardware.biometrics.Flags import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators -import com.android.systemui.res.R +import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.AuthPanelController import com.android.systemui.biometrics.ui.CredentialPasswordView import com.android.systemui.biometrics.ui.CredentialPatternView import com.android.systemui.biometrics.ui.CredentialView import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.filter @@ -40,12 +43,15 @@ object CredentialViewBinder { viewModel: CredentialViewModel, panelViewController: AuthPanelController, animatePanel: Boolean, + legacyCallback: Spaghetti.Callback, maxErrorDuration: Long = 3_000L, requestFocusForInput: Boolean = true, ) { val titleView: TextView = view.requireViewById(R.id.title) val subtitleView: TextView = view.requireViewById(R.id.subtitle) val descriptionView: TextView = view.requireViewById(R.id.description) + val customizedViewContainer: LinearLayout = + view.requireViewById(R.id.customized_view_container) val iconView: ImageView? = view.findViewById(R.id.icon) val errorView: TextView = view.requireViewById(R.id.error) val cancelButton: Button? = view.findViewById(R.id.cancel_button) @@ -76,6 +82,13 @@ object CredentialViewBinder { subtitleView.textOrHide = header.subtitle descriptionView.textOrHide = header.description + if (Flags.customBiometricPrompt() && constraintBp()) { + BiometricCustomizedViewBinder.bind( + customizedViewContainer, + header.contentView, + legacyCallback + ) + } iconView?.setImageDrawable(header.icon) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt index c6d90855e7d2..8b8c90a479d8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialHeaderViewModel.kt @@ -1,6 +1,7 @@ package com.android.systemui.biometrics.ui.viewmodel import android.graphics.drawable.Drawable +import android.hardware.biometrics.PromptContentView import com.android.systemui.biometrics.shared.model.BiometricUserInfo /** View model for the top-level header / info area of BiometricPrompt. */ @@ -9,6 +10,7 @@ interface CredentialHeaderViewModel { val title: String val subtitle: String val description: String + val contentView: PromptContentView? val icon: Drawable val showEmergencyCallButton: Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt index 46be8c74cee3..31af126eb3f0 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt @@ -2,8 +2,11 @@ package com.android.systemui.biometrics.ui.viewmodel import android.content.Context import android.graphics.drawable.Drawable +import android.hardware.biometrics.Flags.customBiometricPrompt +import android.hardware.biometrics.PromptContentView import android.text.InputType import com.android.internal.widget.LockPatternView +import com.android.systemui.Flags.constraintBp import com.android.systemui.biometrics.Utils import com.android.systemui.biometrics.domain.interactor.CredentialStatus import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor @@ -34,14 +37,19 @@ constructor( val header: Flow<CredentialHeaderViewModel> = combine( credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>(), - credentialInteractor.showBpWithoutIconForCredential - ) { request, showBpWithoutIconForCredential -> + credentialInteractor.showTitleOnly + ) { request, showTitleOnly -> + val flagEnabled = customBiometricPrompt() && constraintBp() + val showTitleOnlyForCredential = showTitleOnly && flagEnabled BiometricPromptHeaderViewModelImpl( request, user = request.userInfo, title = request.title, - subtitle = if (showBpWithoutIconForCredential) "" else request.subtitle, - description = if (showBpWithoutIconForCredential) "" else request.description, + subtitle = if (showTitleOnlyForCredential) "" else request.subtitle, + contentView = + if (flagEnabled && !showTitleOnlyForCredential) request.contentView else null, + description = + if (flagEnabled && request.contentView != null) "" else request.description, icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId), showEmergencyCallButton = request.showEmergencyCallButton ) @@ -188,6 +196,7 @@ private class BiometricPromptHeaderViewModelImpl( override val title: String, override val subtitle: String, override val description: String, + override val contentView: PromptContentView?, override val icon: Drawable, override val showEmergencyCallButton: Boolean, ) : CredentialHeaderViewModel diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt index d849b3a44519..94e085479675 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt @@ -20,7 +20,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow /** Provides access to bouncer-related application state. */ @SysUISingleton @@ -29,9 +28,6 @@ class BouncerRepository constructor( private val flags: FeatureFlagsClassic, ) { - /** The user-facing message to show in the bouncer. */ - val message = MutableStateFlow<String?>(null) - /** Whether the user switcher should be displayed within the bouncer UI on large screens. */ val isUserSwitcherVisible: Boolean get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index d8be1afc4dd6..aeb564d53195 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -16,13 +16,8 @@ package com.android.systemui.bouncer.domain.interactor -import android.content.Context import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.domain.interactor.AuthenticationResult -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim import com.android.systemui.bouncer.data.repository.BouncerRepository import com.android.systemui.classifier.FalsingClassifier @@ -31,7 +26,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -41,7 +35,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch /** Encapsulates business logic and application state accessing use-cases. */ @SysUISingleton @@ -49,16 +42,14 @@ class BouncerInteractor @Inject constructor( @Application private val applicationScope: CoroutineScope, - @Application private val applicationContext: Context, private val repository: BouncerRepository, private val authenticationInteractor: AuthenticationInteractor, private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, - private val simBouncerInteractor: SimBouncerInteractor, ) { - /** The user-facing message to show in the bouncer when lockout is not active. */ - val message: StateFlow<String?> = repository.message + private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>() + val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput /** Whether the auto confirm feature is enabled for the currently-selected user. */ val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled @@ -119,25 +110,6 @@ constructor( ) } - fun setMessage(message: String?) { - repository.message.value = message - } - - /** - * Resets the user-facing message back to the default according to the current authentication - * method. - */ - fun resetMessage() { - applicationScope.launch { - setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod())) - } - } - - /** Removes the user-facing message. */ - fun clearMessage() { - setMessage(null) - } - /** * Attempts to authenticate based on the given user input. * @@ -176,50 +148,17 @@ constructor( .async { authenticationInteractor.authenticate(input, tryAutoConfirm) } .await() - if (authenticationInteractor.lockoutEndTimestamp != null) { - clearMessage() - } else if ( + if ( authResult == AuthenticationResult.FAILED || (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm) ) { - showWrongInputMessage() + _onIncorrectBouncerInput.emit(Unit) } return authResult } - /** - * Shows the a message notifying the user that their credentials input is wrong. - * - * Callers should use this instead of [authenticate] when they know ahead of time that an auth - * attempt will fail but aren't interested in the other side effects like triggering lockout. - * For example, if the user entered a pattern that's too short, the system can show the error - * message without having the attempt trigger lockout. - */ - private suspend fun showWrongInputMessage() { - setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod())) - } - /** Notifies that the input method editor (software keyboard) has been hidden by the user. */ suspend fun onImeHiddenByUser() { _onImeHiddenByUser.emit(Unit) } - - private fun promptMessage(authMethod: AuthenticationMethodModel): String { - return when (authMethod) { - is Sim -> simBouncerInteractor.getDefaultMessage() - is Pin -> applicationContext.getString(R.string.keyguard_enter_your_pin) - is Password -> applicationContext.getString(R.string.keyguard_enter_your_password) - is Pattern -> applicationContext.getString(R.string.keyguard_enter_your_pattern) - else -> "" - } - } - - private fun wrongInputMessage(authMethod: AuthenticationMethodModel): String { - return when (authMethod) { - is Pin -> applicationContext.getString(R.string.kg_wrong_pin) - is Password -> applicationContext.getString(R.string.kg_wrong_password) - is Pattern -> applicationContext.getString(R.string.kg_wrong_pattern) - else -> "" - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt index 7f6fc914e92b..d20c60724822 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt @@ -33,15 +33,17 @@ import com.android.systemui.bouncer.shared.model.Message import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.flags.SystemPropertiesHelper import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.user.data.repository.UserRepository -import com.android.systemui.util.kotlin.Quint +import com.android.systemui.util.kotlin.Sextuple +import com.android.systemui.util.kotlin.combine import javax.inject.Inject import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -56,6 +58,7 @@ private const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update" private const val TAG = "BouncerMessageInteractor" /** Handles business logic for the primary bouncer message area. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class BouncerMessageInteractor @Inject @@ -63,23 +66,24 @@ constructor( private val repository: BouncerMessageRepository, private val userRepository: UserRepository, private val countDownTimerUtil: CountDownTimerUtil, - private val updateMonitor: KeyguardUpdateMonitor, + updateMonitor: KeyguardUpdateMonitor, trustRepository: TrustRepository, biometricSettingsRepository: BiometricSettingsRepository, private val systemPropertiesHelper: SystemPropertiesHelper, primaryBouncerInteractor: PrimaryBouncerInteractor, @Application private val applicationScope: CoroutineScope, private val facePropertyRepository: FacePropertyRepository, - deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, + private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, faceAuthRepository: DeviceEntryFaceAuthRepository, private val securityModel: KeyguardSecurityModel, ) { - private val isFingerprintAuthCurrentlyAllowed = - deviceEntryFingerprintAuthRepository.isLockedOut - .isFalse() - .and(biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed) - .stateIn(applicationScope, SharingStarted.Eagerly, false) + private val isFingerprintAuthCurrentlyAllowedOnBouncer = + deviceEntryFingerprintAuthInteractor.isFingerprintCurrentlyAllowedOnBouncer.stateIn( + applicationScope, + SharingStarted.Eagerly, + false + ) private val currentSecurityMode get() = securityModel.getSecurityMode(currentUserId) @@ -99,13 +103,13 @@ constructor( BiometricSourceType.FACE -> BouncerMessageStrings.incorrectFaceInput( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() else -> BouncerMessageStrings.defaultMessage( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } @@ -144,11 +148,12 @@ constructor( biometricSettingsRepository.authenticationFlags, trustRepository.isCurrentUserTrustManaged, isAnyBiometricsEnabledAndEnrolled, - deviceEntryFingerprintAuthRepository.isLockedOut, + deviceEntryFingerprintAuthInteractor.isLockedOut, faceAuthRepository.isLockedOut, - ::Quint + isFingerprintAuthCurrentlyAllowedOnBouncer, + ::Sextuple ) - .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut) -> + .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut, _) -> val isTrustUsuallyManaged = trustRepository.isCurrentUserTrustUsuallyManaged.value val trustOrBiometricsAvailable = (isTrustUsuallyManaged || biometricsEnrolledAndEnabled) @@ -193,14 +198,14 @@ constructor( } else { BouncerMessageStrings.faceLockedOut( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } } else if (flags.isSomeAuthRequiredAfterAdaptiveAuthRequest) { BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if ( @@ -209,19 +214,19 @@ constructor( ) { BouncerMessageStrings.nonStrongAuthTimeout( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterUserRequest) { BouncerMessageStrings.trustAgentDisabled( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterTrustAgentExpired) { BouncerMessageStrings.trustAgentDisabled( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) { @@ -265,7 +270,7 @@ constructor( repository.setMessage( BouncerMessageStrings.incorrectSecurityInput( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() ) @@ -274,14 +279,22 @@ constructor( fun setFingerprintAcquisitionMessage(value: String?) { if (!Flags.revampedBouncerMessages()) return repository.setMessage( - defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value) + defaultMessage( + currentSecurityMode, + value, + isFingerprintAuthCurrentlyAllowedOnBouncer.value + ) ) } fun setFaceAcquisitionMessage(value: String?) { if (!Flags.revampedBouncerMessages()) return repository.setMessage( - defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value) + defaultMessage( + currentSecurityMode, + value, + isFingerprintAuthCurrentlyAllowedOnBouncer.value + ) ) } @@ -289,7 +302,11 @@ constructor( if (!Flags.revampedBouncerMessages()) return repository.setMessage( - defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value) + defaultMessage( + currentSecurityMode, + value, + isFingerprintAuthCurrentlyAllowedOnBouncer.value + ) ) } @@ -297,7 +314,7 @@ constructor( get() = BouncerMessageStrings.defaultMessage( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() @@ -355,11 +372,6 @@ open class CountDownTimerUtil @Inject constructor() { private fun Flow<Boolean>.or(anotherFlow: Flow<Boolean>) = this.combine(anotherFlow) { a, b -> a || b } -private fun Flow<Boolean>.and(anotherFlow: Flow<Boolean>) = - this.combine(anotherFlow) { a, b -> a && b } - -private fun Flow<Boolean>.isFalse() = this.map { !it } - private fun defaultMessage( securityMode: SecurityMode, secondaryMessage: String?, diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt index f3903ded7cf4..aebc50f92e8d 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.bouncer.ui import android.app.AlertDialog import android.content.Context +import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModelModule import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -30,6 +31,7 @@ import dagger.Provides includes = [ BouncerViewModelModule::class, + BouncerMessageViewModelModule::class, ], ) interface BouncerViewModule { diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt index 0d7f6dcce1c7..4fbf735a62a2 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -57,17 +57,11 @@ sealed class AuthMethodBouncerViewModel( */ @get:StringRes abstract val lockoutMessageId: Int - /** Notifies that the UI has been shown to the user. */ - fun onShown() { - interactor.resetMessage() - } - /** * Notifies that the UI has been hidden from the user (after any transitions have completed). */ open fun onHidden() { clearInput() - interactor.resetMessage() } /** Notifies that the user has placed down a pointer. */ diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt new file mode 100644 index 000000000000..6cb9b16e2f9b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt @@ -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 com.android.systemui.bouncer.ui.viewmodel + +import android.content.Context +import android.util.PluralsMessageFormatter +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor +import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags +import com.android.systemui.bouncer.shared.model.BouncerMessagePair +import com.android.systemui.bouncer.shared.model.BouncerMessageStrings +import com.android.systemui.bouncer.shared.model.primaryMessage +import com.android.systemui.bouncer.shared.model.secondaryMessage +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason +import com.android.systemui.deviceentry.shared.model.FaceFailureMessage +import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage +import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage +import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage +import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage +import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown +import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel +import com.android.systemui.user.ui.viewmodel.UserViewModel +import com.android.systemui.util.kotlin.Utils.Companion.sample +import com.android.systemui.util.time.SystemClock +import dagger.Module +import dagger.Provides +import kotlin.math.ceil +import kotlin.math.max +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** Holds UI state for the 2-line status message shown on the bouncer. */ +@OptIn(ExperimentalCoroutinesApi::class) +class BouncerMessageViewModel( + @Application private val applicationContext: Context, + @Application private val applicationScope: CoroutineScope, + private val bouncerInteractor: BouncerInteractor, + private val simBouncerInteractor: SimBouncerInteractor, + private val authenticationInteractor: AuthenticationInteractor, + selectedUser: Flow<UserViewModel>, + private val clock: SystemClock, + private val biometricMessageInteractor: BiometricMessageInteractor, + private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, + private val deviceEntryInteractor: DeviceEntryInteractor, + private val fingerprintInteractor: DeviceEntryFingerprintAuthInteractor, + flags: ComposeBouncerFlags, +) { + /** + * A message shown when the user has attempted the wrong credential too many times and now must + * wait a while before attempting to authenticate again. + * + * This is updated every second (countdown) during the lockout. When lockout is not active, this + * is `null` and no lockout message should be shown. + */ + private val lockoutMessage: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null) + + /** Whether there is a lockout message that is available to be shown in the status message. */ + val isLockoutMessagePresent: Flow<Boolean> = lockoutMessage.map { it != null } + + /** The user-facing message to show in the bouncer. */ + val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null) + + /** Initializes the bouncer message to default whenever it is shown. */ + fun onShown() { + showDefaultMessage() + } + + /** Reset the message shown on the bouncer to the default message. */ + fun showDefaultMessage() { + resetToDefault.tryEmit(Unit) + } + + private val resetToDefault = MutableSharedFlow<Unit>(replay = 1) + + private var lockoutCountdownJob: Job? = null + + private fun defaultBouncerMessageInitializer() { + applicationScope.launch { + resetToDefault.emit(Unit) + authenticationInteractor.authenticationMethod + .flatMapLatest { authMethod -> + if (authMethod == AuthenticationMethodModel.Sim) { + resetToDefault.map { + MessageViewModel(simBouncerInteractor.getDefaultMessage()) + } + } else if (authMethod.isSecure) { + combine( + deviceEntryInteractor.deviceEntryRestrictionReason, + lockoutMessage, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer, + resetToDefault, + ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ -> + lockoutMsg + ?: deviceEntryRestrictedReason.toMessage( + authMethod, + isFpAllowedInBouncer + ) + } + } else { + emptyFlow() + } + } + .collectLatest { messageViewModel -> message.value = messageViewModel } + } + } + + private fun listenForSimBouncerEvents() { + // Listen for any events from the SIM bouncer and update the message shown on the bouncer. + applicationScope.launch { + authenticationInteractor.authenticationMethod + .flatMapLatest { authMethod -> + if (authMethod == AuthenticationMethodModel.Sim) { + simBouncerInteractor.bouncerMessageChanged.map { simMsg -> + simMsg?.let { MessageViewModel(it) } + } + } else { + emptyFlow() + } + } + .collectLatest { + if (it != null) { + message.value = it + } else { + resetToDefault.emit(Unit) + } + } + } + } + + private fun listenForFaceMessages() { + // Listen for any events from face authentication and update the message shown on the + // bouncer. + applicationScope.launch { + biometricMessageInteractor.faceMessage + .sample( + authenticationInteractor.authenticationMethod, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer, + ) + .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) -> + val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong() + val defaultPrimaryMessage = + BouncerMessageStrings.defaultMessage( + authMethod, + fingerprintAllowedOnBouncer + ) + .primaryMessage + .toResString() + message.value = + when (faceMessage) { + is FaceTimeoutMessage -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = faceMessage.message, + isUpdateAnimated = true + ) + is FaceLockoutMessage -> + if (isFaceAuthStrong) + BouncerMessageStrings.class3AuthLockedOut(authMethod) + .toMessage() + else + BouncerMessageStrings.faceLockedOut( + authMethod, + fingerprintAllowedOnBouncer + ) + .toMessage() + is FaceFailureMessage -> + BouncerMessageStrings.incorrectFaceInput( + authMethod, + fingerprintAllowedOnBouncer + ) + .toMessage() + else -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = faceMessage.message, + isUpdateAnimated = false + ) + } + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } + } + } + + private fun listenForFingerprintMessages() { + applicationScope.launch { + // Listen for any events from fingerprint authentication and update the message shown + // on the bouncer. + biometricMessageInteractor.fingerprintMessage + .sample( + authenticationInteractor.authenticationMethod, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer + ) + .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) -> + val defaultPrimaryMessage = + BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed) + .primaryMessage + .toResString() + message.value = + when (fingerprintMessage) { + is FingerprintLockoutMessage -> + BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage() + is FingerprintFailureMessage -> + BouncerMessageStrings.incorrectFingerprintInput(authMethod) + .toMessage() + else -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = fingerprintMessage.message, + isUpdateAnimated = false + ) + } + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } + } + } + + private fun listenForBouncerEvents() { + // Keeps the lockout message up-to-date. + applicationScope.launch { + bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() } + } + + // Listens to relevant bouncer events + applicationScope.launch { + bouncerInteractor.onIncorrectBouncerInput + .sample( + authenticationInteractor.authenticationMethod, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer + ) + .collectLatest { (_, authMethod, isFingerprintAllowed) -> + message.emit( + BouncerMessageStrings.incorrectSecurityInput( + authMethod, + isFingerprintAllowed + ) + .toMessage() + ) + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } + } + } + + private fun DeviceEntryRestrictionReason?.toMessage( + authMethod: AuthenticationMethodModel, + isFingerprintAllowedOnBouncer: Boolean, + ): MessageViewModel { + return when (this) { + DeviceEntryRestrictionReason.UserLockdown -> + BouncerMessageStrings.authRequiredAfterUserLockdown(authMethod) + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot -> + BouncerMessageStrings.authRequiredAfterReboot(authMethod) + DeviceEntryRestrictionReason.PolicyLockdown -> + BouncerMessageStrings.authRequiredAfterAdminLockdown(authMethod) + DeviceEntryRestrictionReason.UnattendedUpdate -> + BouncerMessageStrings.authRequiredForUnattendedUpdate(authMethod) + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate -> + BouncerMessageStrings.authRequiredForMainlineUpdate(authMethod) + DeviceEntryRestrictionReason.SecurityTimeout -> + BouncerMessageStrings.authRequiredAfterPrimaryAuthTimeout(authMethod) + DeviceEntryRestrictionReason.StrongBiometricsLockedOut -> + BouncerMessageStrings.class3AuthLockedOut(authMethod) + DeviceEntryRestrictionReason.NonStrongFaceLockedOut -> + BouncerMessageStrings.faceLockedOut(authMethod, isFingerprintAllowedOnBouncer) + DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout -> + BouncerMessageStrings.nonStrongAuthTimeout( + authMethod, + isFingerprintAllowedOnBouncer + ) + DeviceEntryRestrictionReason.TrustAgentDisabled -> + BouncerMessageStrings.trustAgentDisabled(authMethod, isFingerprintAllowedOnBouncer) + DeviceEntryRestrictionReason.AdaptiveAuthRequest -> + BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest( + authMethod, + isFingerprintAllowedOnBouncer + ) + else -> BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowedOnBouncer) + }.toMessage() + } + + private fun BouncerMessagePair.toMessage(): MessageViewModel { + val primaryMsg = this.primaryMessage.toResString() + val secondaryMsg = + if (this.secondaryMessage == 0) "" else this.secondaryMessage.toResString() + return MessageViewModel(primaryMsg, secondaryText = secondaryMsg, isUpdateAnimated = true) + } + + /** Shows the countdown message and refreshes it every second. */ + private fun startLockoutCountdown() { + lockoutCountdownJob?.cancel() + lockoutCountdownJob = + applicationScope.launch { + authenticationInteractor.authenticationMethod.collectLatest { authMethod -> + do { + val remainingSeconds = remainingLockoutSeconds() + val authLockedOutMsg = + BouncerMessageStrings.primaryAuthLockedOut(authMethod) + lockoutMessage.value = + if (remainingSeconds > 0) { + MessageViewModel( + text = + kg_too_many_failed_attempts_countdown.toPluralString( + mutableMapOf<String, Any>( + Pair("count", remainingSeconds) + ) + ), + secondaryText = authLockedOutMsg.secondaryMessage.toResString(), + isUpdateAnimated = false + ) + } else { + null + } + delay(1.seconds) + } while (remainingSeconds > 0) + lockoutCountdownJob = null + } + } + } + + private fun remainingLockoutSeconds(): Int { + val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0 + val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime()) + return ceil(remainingMs / 1000f).toInt() + } + + private fun Int.toPluralString(formatterArgs: Map<String, Any>): String = + PluralsMessageFormatter.format(applicationContext.resources, formatterArgs, this) + + private fun Int.toResString(): String = applicationContext.getString(this) + + init { + if (flags.isComposeBouncerOrSceneContainerEnabled()) { + applicationScope.launch { + // Update the lockout countdown whenever the selected user is switched. + selectedUser.collect { startLockoutCountdown() } + } + + defaultBouncerMessageInitializer() + + listenForSimBouncerEvents() + listenForBouncerEvents() + listenForFaceMessages() + listenForFingerprintMessages() + } + } + + companion object { + private const val MESSAGE_DURATION = 2000L + } +} + +/** Data class that represents the status message show on the bouncer. */ +data class MessageViewModel( + val text: String, + val secondaryText: String? = null, + /** + * Whether updates to the message should be cross-animated from one message to another. + * + * If `false`, no animation should be applied, the message text should just be replaced + * instantly. + */ + val isUpdateAnimated: Boolean = true, +) + +@OptIn(ExperimentalCoroutinesApi::class) +@Module +object BouncerMessageViewModelModule { + + @Provides + @SysUISingleton + fun viewModel( + @Application applicationContext: Context, + @Application applicationScope: CoroutineScope, + bouncerInteractor: BouncerInteractor, + simBouncerInteractor: SimBouncerInteractor, + authenticationInteractor: AuthenticationInteractor, + clock: SystemClock, + biometricMessageInteractor: BiometricMessageInteractor, + faceAuthInteractor: DeviceEntryFaceAuthInteractor, + deviceEntryInteractor: DeviceEntryInteractor, + fingerprintInteractor: DeviceEntryFingerprintAuthInteractor, + flags: ComposeBouncerFlags, + userSwitcherViewModel: UserSwitcherViewModel, + ): BouncerMessageViewModel { + return BouncerMessageViewModel( + applicationContext = applicationContext, + applicationScope = applicationScope, + bouncerInteractor = bouncerInteractor, + simBouncerInteractor = simBouncerInteractor, + authenticationInteractor = authenticationInteractor, + clock = clock, + biometricMessageInteractor = biometricMessageInteractor, + faceAuthInteractor = faceAuthInteractor, + deviceEntryInteractor = deviceEntryInteractor, + fingerprintInteractor = fingerprintInteractor, + flags = flags, + selectedUser = userSwitcherViewModel.selectedUser, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt index 62875783ef5f..5c07cc57c620 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -21,7 +21,6 @@ import android.app.admin.DevicePolicyResources import android.content.Context import android.graphics.Bitmap import androidx.core.graphics.drawable.toBitmap -import com.android.internal.R import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationWipeModel @@ -40,18 +39,12 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.user.ui.viewmodel.UserViewModel -import com.android.systemui.util.time.SystemClock import dagger.Module import dagger.Provides -import kotlin.math.ceil -import kotlin.math.max -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -72,13 +65,13 @@ class BouncerViewModel( private val simBouncerInteractor: SimBouncerInteractor, private val authenticationInteractor: AuthenticationInteractor, private val selectedUserInteractor: SelectedUserInteractor, + private val devicePolicyManager: DevicePolicyManager, + bouncerMessageViewModel: BouncerMessageViewModel, flags: ComposeBouncerFlags, selectedUser: Flow<UserViewModel>, users: Flow<List<UserViewModel>>, userSwitcherMenu: Flow<List<UserActionViewModel>>, actionButton: Flow<BouncerActionButtonModel?>, - private val clock: SystemClock, - private val devicePolicyManager: DevicePolicyManager, ) { val selectedUserImage: StateFlow<Bitmap?> = selectedUser @@ -89,6 +82,8 @@ class BouncerViewModel( initialValue = null, ) + val message: BouncerMessageViewModel = bouncerMessageViewModel + val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> = combine( users, @@ -163,24 +158,6 @@ class BouncerViewModel( ) /** - * A message shown when the user has attempted the wrong credential too many times and now must - * wait a while before attempting to authenticate again. - * - * This is updated every second (countdown) during the lockout duration. When lockout is not - * active, this is `null` and no lockout message should be shown. - */ - private val lockoutMessage = MutableStateFlow<String?>(null) - - /** The user-facing message to show in the bouncer. */ - val message: StateFlow<MessageViewModel> = - combine(bouncerInteractor.message, lockoutMessage) { _, _ -> createMessageViewModel() } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = createMessageViewModel(), - ) - - /** * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not * be shown. */ @@ -222,31 +199,16 @@ class BouncerViewModel( ) private val isInputEnabled: StateFlow<Boolean> = - lockoutMessage - .map { it == null } + bouncerMessageViewModel.isLockoutMessagePresent + .map { lockoutMessagePresent -> !lockoutMessagePresent } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = authenticationInteractor.lockoutEndTimestamp == null, ) - private var lockoutCountdownJob: Job? = null - init { if (flags.isComposeBouncerOrSceneContainerEnabled()) { - // Keeps the lockout dialog up-to-date. - applicationScope.launch { - bouncerInteractor.onLockoutStarted.collect { - showLockoutDialog() - startLockoutCountdown() - } - } - - applicationScope.launch { - // Update the lockout countdown whenever the selected user is switched. - selectedUser.collect { startLockoutCountdown() } - } - // Keeps the upcoming wipe dialog up-to-date. applicationScope.launch { authenticationInteractor.upcomingWipe.collect { wipeModel -> @@ -256,48 +218,6 @@ class BouncerViewModel( } } - private fun showLockoutDialog() { - applicationScope.launch { - val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value - lockoutDialogMessage.value = - authMethodViewModel.value?.lockoutMessageId?.let { messageId -> - applicationContext.getString( - messageId, - failedAttempts, - remainingLockoutSeconds() - ) - } - } - } - - /** Shows the countdown message and refreshes it every second. */ - private fun startLockoutCountdown() { - lockoutCountdownJob?.cancel() - lockoutCountdownJob = - applicationScope.launch { - do { - val remainingSeconds = remainingLockoutSeconds() - lockoutMessage.value = - if (remainingSeconds > 0) { - applicationContext.getString( - R.string.lockscreen_too_many_failed_attempts_countdown, - remainingSeconds, - ) - } else { - null - } - delay(1.seconds) - } while (remainingSeconds > 0) - lockoutCountdownJob = null - } - } - - private fun remainingLockoutSeconds(): Int { - val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0 - val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime()) - return ceil(remainingMs / 1000f).toInt() - } - private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean { return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel } @@ -306,15 +226,6 @@ class BouncerViewModel( return authMethod !is PasswordBouncerViewModel } - private fun createMessageViewModel(): MessageViewModel { - val isLockedOut = lockoutMessage.value != null - return MessageViewModel( - // A lockout message takes precedence over the non-lockout message. - text = lockoutMessage.value ?: bouncerInteractor.message.value ?: "", - isUpdateAnimated = !isLockedOut, - ) - } - private fun getChildViewModel( authenticationMethod: AuthenticationMethodModel, ): AuthMethodBouncerViewModel? { @@ -336,7 +247,8 @@ class BouncerViewModel( interactor = bouncerInteractor, isInputEnabled = isInputEnabled, simBouncerInteractor = simBouncerInteractor, - authenticationMethod = authenticationMethod + authenticationMethod = authenticationMethod, + onIntentionalUserInput = ::onIntentionalUserInput ) is AuthenticationMethodModel.Sim -> PinBouncerViewModel( @@ -346,6 +258,7 @@ class BouncerViewModel( isInputEnabled = isInputEnabled, simBouncerInteractor = simBouncerInteractor, authenticationMethod = authenticationMethod, + onIntentionalUserInput = ::onIntentionalUserInput ) is AuthenticationMethodModel.Password -> PasswordBouncerViewModel( @@ -354,6 +267,7 @@ class BouncerViewModel( interactor = bouncerInteractor, inputMethodInteractor = inputMethodInteractor, selectedUserInteractor = selectedUserInteractor, + onIntentionalUserInput = ::onIntentionalUserInput ) is AuthenticationMethodModel.Pattern -> PatternBouncerViewModel( @@ -361,11 +275,17 @@ class BouncerViewModel( viewModelScope = newViewModelScope, interactor = bouncerInteractor, isInputEnabled = isInputEnabled, + onIntentionalUserInput = ::onIntentionalUserInput ) else -> null } } + private fun onIntentionalUserInput() { + message.showDefaultMessage() + bouncerInteractor.onIntentionalUserInput() + } + private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope { return CoroutineScope( SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher @@ -437,18 +357,6 @@ class BouncerViewModel( } } - data class MessageViewModel( - val text: String, - - /** - * Whether updates to the message should be cross-animated from one message to another. - * - * If `false`, no animation should be applied, the message text should just be replaced - * instantly. - */ - val isUpdateAnimated: Boolean, - ) - data class DialogViewModel( val text: String, @@ -480,8 +388,8 @@ object BouncerViewModelModule { selectedUserInteractor: SelectedUserInteractor, flags: ComposeBouncerFlags, userSwitcherViewModel: UserSwitcherViewModel, - clock: SystemClock, devicePolicyManager: DevicePolicyManager, + bouncerMessageViewModel: BouncerMessageViewModel, ): BouncerViewModel { return BouncerViewModel( applicationContext = applicationContext, @@ -497,8 +405,8 @@ object BouncerViewModelModule { users = userSwitcherViewModel.users, userSwitcherMenu = userSwitcherViewModel.menu, actionButton = actionButtonInteractor.actionButton, - clock = clock, devicePolicyManager = devicePolicyManager, + bouncerMessageViewModel = bouncerMessageViewModel, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt index b42eda108d54..052fb6b3c4d7 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt @@ -40,6 +40,7 @@ class PasswordBouncerViewModel( viewModelScope: CoroutineScope, isInputEnabled: StateFlow<Boolean>, interactor: BouncerInteractor, + private val onIntentionalUserInput: () -> Unit, private val inputMethodInteractor: InputMethodInteractor, private val selectedUserInteractor: SelectedUserInteractor, ) : @@ -96,12 +97,8 @@ class PasswordBouncerViewModel( /** Notifies that the user has changed the password input. */ fun onPasswordInputChanged(newPassword: String) { - if (this.password.value.isEmpty() && newPassword.isNotEmpty()) { - interactor.clearMessage() - } - if (newPassword.isNotEmpty()) { - interactor.onIntentionalUserInput() + onIntentionalUserInput() } _password.value = newPassword diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt index 69f8032ef4f2..a4016005a756 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt @@ -40,6 +40,7 @@ class PatternBouncerViewModel( viewModelScope: CoroutineScope, interactor: BouncerInteractor, isInputEnabled: StateFlow<Boolean>, + private val onIntentionalUserInput: () -> Unit, ) : AuthMethodBouncerViewModel( viewModelScope = viewModelScope, @@ -84,7 +85,7 @@ class PatternBouncerViewModel( /** Notifies that the user has started a drag gesture across the dot grid. */ fun onDragStart() { - interactor.clearMessage() + onIntentionalUserInput() } /** diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt index e910a9271ee2..62da5c0e5675 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -41,6 +41,7 @@ class PinBouncerViewModel( viewModelScope: CoroutineScope, interactor: BouncerInteractor, isInputEnabled: StateFlow<Boolean>, + private val onIntentionalUserInput: () -> Unit, private val simBouncerInteractor: SimBouncerInteractor, authenticationMethod: AuthenticationMethodModel, ) : @@ -131,11 +132,8 @@ class PinBouncerViewModel( /** Notifies that the user clicked on a PIN button with the given digit value. */ fun onPinButtonClicked(input: Int) { val pinInput = mutablePinInput.value - if (pinInput.isEmpty()) { - interactor.clearMessage() - } - interactor.onIntentionalUserInput() + onIntentionalUserInput() mutablePinInput.value = pinInput.append(input) tryAuthenticate(useAutoConfirm = true) @@ -149,7 +147,6 @@ class PinBouncerViewModel( /** Notifies that the user long-pressed the backspace button. */ fun onBackspaceButtonLongPressed() { clearInput() - interactor.clearMessage() } /** Notifies that the user clicked the "enter" button. */ @@ -173,7 +170,6 @@ class PinBouncerViewModel( /** Resets the sim screen and shows a default message. */ private fun onResetSimFlow() { simBouncerInteractor.resetSimPukUserInput() - interactor.resetMessage() clearInput() } diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt index 3063ebd60b0c..fdd98bec0a2d 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt @@ -18,12 +18,8 @@ package com.android.systemui.common.shared.model /** Models the bounds of the notification container. */ data class NotificationContainerBounds( - /** The position of the left of the container in its window coordinate system, in pixels. */ - val left: Float = 0f, /** The position of the top of the container in its window coordinate system, in pixels. */ val top: Float = 0f, - /** The position of the right of the container in its window coordinate system, in pixels. */ - val right: Float = 0f, /** The position of the bottom of the container in its window coordinate system, in pixels. */ val bottom: Float = 0f, /** Whether any modifications to top/bottom should be smoothly animated. */ diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt index 964eb6f3a613..578389b57a99 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt @@ -54,6 +54,18 @@ constructor( } /** + * Returns a [Flow] that emits a dimension pixel size that is kept in sync with the device + * configuration. + * + * @see android.content.res.Resources.getDimensionPixelSize + */ + fun getDimensionPixelOffset(@DimenRes id: Int): Flow<Int> { + return configurationController.onDensityOrFontScaleChanged.emitOnStart().map { + context.resources.getDimensionPixelOffset(id) + } + } + + /** * Returns a [Flow] that emits a color that is kept in sync with the device theme. * * @see Utils.getColorAttrDefaultColor diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index bfe751af7154..afa7c37c648e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -16,24 +16,36 @@ package com.android.systemui.communal.ui.viewmodel +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Resources +import android.util.Log +import androidx.activity.result.ActivityResultLauncher import com.android.internal.logging.UiEventLogger import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.dagger.MediaModule +import com.android.systemui.res.R import javax.inject.Inject import javax.inject.Named +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -45,6 +57,7 @@ constructor( @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, private val uiEventLogger: UiEventLogger, @CommunalLog logBuffer: LogBuffer, + @Background private val backgroundDispatcher: CoroutineDispatcher, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { private val logger = Logger(logBuffer, "CommunalEditModeViewModel") @@ -86,10 +99,77 @@ constructor( uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } - /** Returns the widget categories to show on communal hub. */ - val getCommunalWidgetCategories: Int - get() = communalSettingsInteractor.communalWidgetCategories.value + /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */ + suspend fun onOpenWidgetPicker( + resources: Resources, + packageManager: PackageManager, + activityLauncher: ActivityResultLauncher<Intent> + ): Boolean = + withContext(backgroundDispatcher) { + val widgets = communalInteractor.widgetContent.first() + val excludeList = widgets.mapTo(ArrayList()) { it.providerInfo } + getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let { + try { + activityLauncher.launch(it) + return@withContext true + } catch (e: Exception) { + Log.e(TAG, "Failed to launch widget picker activity", e) + } + } + false + } + + private fun getWidgetPickerActivityIntent( + resources: Resources, + packageManager: PackageManager, + excludeList: ArrayList<AppWidgetProviderInfo> + ): Intent? { + val packageName = + getLauncherPackageName(packageManager) + ?: run { + Log.e(TAG, "Couldn't resolve launcher package name") + return@getWidgetPickerActivityIntent null + } + + return Intent(Intent.ACTION_PICK).apply { + setPackage(packageName) + putExtra( + EXTRA_DESIRED_WIDGET_WIDTH, + resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width) + ) + putExtra( + EXTRA_DESIRED_WIDGET_HEIGHT, + resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height) + ) + putExtra( + AppWidgetManager.EXTRA_CATEGORY_FILTER, + communalSettingsInteractor.communalWidgetCategories.value + ) + putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE) + putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList) + } + } + + private fun getLauncherPackageName(packageManager: PackageManager): String? { + return packageManager + .resolveActivity( + Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }, + PackageManager.MATCH_DEFAULT_ONLY + ) + ?.activityInfo + ?.packageName + } /** Sets whether edit mode is currently open */ fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen) + + companion object { + private const val TAG = "CommunalEditModeViewModel" + + private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width" + private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height" + private const val EXTRA_UI_SURFACE_KEY = "ui_surface" + private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub" + const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets" + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index b6ad26b24dc7..ba18f0125a0a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -16,9 +16,7 @@ package com.android.systemui.communal.widgets -import android.appwidget.AppWidgetManager import android.content.Intent -import android.content.pm.PackageManager import android.os.Bundle import android.os.RemoteException import android.util.Log @@ -32,6 +30,8 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +import com.android.app.tracing.coroutines.launch import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.theme.PlatformTheme import com.android.internal.logging.UiEventLogger @@ -43,8 +43,8 @@ import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtra import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog -import com.android.systemui.res.R import javax.inject.Inject +import kotlinx.coroutines.launch /** An Activity for editing the widgets that appear in hub mode. */ class EditWidgetsActivity @@ -57,11 +57,8 @@ constructor( @CommunalLog logBuffer: LogBuffer, ) : ComponentActivity() { companion object { - private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" - private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width" - private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height" - private const val TAG = "EditWidgetsActivity" + private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" const val EXTRA_PRESELECTED_KEY = "preselected_key" } @@ -136,39 +133,13 @@ constructor( } private fun onOpenWidgetPicker() { - val intent = Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) } - packageManager - .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) - ?.activityInfo - ?.packageName - ?.let { packageName -> - try { - addWidgetActivityLauncher.launch( - Intent(Intent.ACTION_PICK).apply { - setPackage(packageName) - putExtra( - EXTRA_DESIRED_WIDGET_WIDTH, - resources.getDimensionPixelSize( - R.dimen.communal_widget_picker_desired_width - ) - ) - putExtra( - EXTRA_DESIRED_WIDGET_HEIGHT, - resources.getDimensionPixelSize( - R.dimen.communal_widget_picker_desired_height - ) - ) - putExtra( - AppWidgetManager.EXTRA_CATEGORY_FILTER, - communalViewModel.getCommunalWidgetCategories - ) - } - ) - } catch (e: Exception) { - Log.e(TAG, "Failed to launch widget picker activity", e) - } - } - ?: run { Log.e(TAG, "Couldn't resolve launcher package name") } + lifecycleScope.launch { + communalViewModel.onOpenWidgetPicker( + resources, + packageManager, + addWidgetActivityLauncher + ) + } } private fun onEditDone() { diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt index 4c1e77bc47f8..778d8cf56648 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetInteractionHandler.kt @@ -16,9 +16,14 @@ package com.android.systemui.communal.widgets +import android.app.ActivityOptions import android.app.PendingIntent +import android.content.Intent +import android.util.Pair import android.view.View import android.widget.RemoteViews +import androidx.core.util.component1 +import androidx.core.util.component2 import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.common.ui.view.getNearestParent import com.android.systemui.plugins.ActivityStarter @@ -33,21 +38,33 @@ constructor( view: View, pendingIntent: PendingIntent, response: RemoteViews.RemoteResponse - ): Boolean = - when { - pendingIntent.isActivity -> startActivity(view, pendingIntent) - else -> - RemoteViews.startPendingIntent(view, pendingIntent, response.getLaunchOptions(view)) + ): Boolean { + val launchOptions = response.getLaunchOptions(view) + return when { + pendingIntent.isActivity -> + // Forward the fill-in intent and activity options retrieved from the response + // to populate the pending intent, so that list items can launch respective + // activities. + startActivity(view, pendingIntent, launchOptions) + else -> RemoteViews.startPendingIntent(view, pendingIntent, launchOptions) } + } - private fun startActivity(view: View, pendingIntent: PendingIntent): Boolean { + private fun startActivity( + view: View, + pendingIntent: PendingIntent, + launchOptions: Pair<Intent, ActivityOptions>, + ): Boolean { val hostView = view.getNearestParent<CommunalAppWidgetHostView>() val animationController = hostView?.let(ActivityTransitionAnimator.Controller::fromView) + val (fillInIntent, activityOptions) = launchOptions activityStarter.startPendingIntentMaybeDismissingKeyguard( pendingIntent, /* intentSentUiThreadCallback = */ null, - animationController + animationController, + fillInIntent, + activityOptions.toBundle(), ) return true } diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt index 7e5b26732e00..c110d06aeedb 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsRequestReceiver.kt @@ -28,7 +28,6 @@ import android.os.UserHandle import android.service.controls.Control import android.service.controls.ControlsProviderService import android.util.Log -import java.lang.ClassCastException /** * Proxy to launch in user 0 @@ -61,22 +60,28 @@ class ControlsRequestReceiver : BroadcastReceiver() { } val targetComponent = try { - intent.getParcelableExtra<ComponentName>(Intent.EXTRA_COMPONENT_NAME) - } catch (e: ClassCastException) { + intent.getParcelableExtra(Intent.EXTRA_COMPONENT_NAME, ComponentName::class.java) + } catch (e: Exception) { Log.e(TAG, "Malformed intent extra ComponentName", e) return + } ?: run { + Log.e(TAG, "Null target component") + return } val control = try { - intent.getParcelableExtra<Control>(ControlsProviderService.EXTRA_CONTROL) - } catch (e: ClassCastException) { + intent.getParcelableExtra(ControlsProviderService.EXTRA_CONTROL, Control::class.java) + } catch (e: Exception) { Log.e(TAG, "Malformed intent extra Control", e) return + } ?: run { + Log.e(TAG, "Null control") + return } - val packageName = targetComponent?.packageName + val packageName = targetComponent.packageName - if (packageName == null || !isPackageInForeground(context, packageName)) { + if (!isPackageInForeground(context, packageName)) { return } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt index 805999397282..c4e0ef7d082d 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt @@ -29,6 +29,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @OptIn(ExperimentalCoroutinesApi::class) @@ -72,4 +74,14 @@ constructor( */ val isSensorUnderDisplay = fingerprintPropertyRepository.sensorType.map(FingerprintSensorType::isUdfps) + + /** Whether fingerprint authentication is currently allowed while on the bouncer. */ + val isFingerprintCurrentlyAllowedOnBouncer = + isSensorUnderDisplay.flatMapLatest { sensorBelowDisplay -> + if (sensorBelowDisplay) { + flowOf(false) + } else { + isFingerprintAuthCurrentlyAllowed + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 298da1359728..1bcee74d70fc 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -23,13 +23,11 @@ import com.android.server.notification.Flags.crossAppPoliteNotifications import com.android.server.notification.Flags.politeNotifications import com.android.server.notification.Flags.vibrateWhileUnlocked import com.android.systemui.Flags.FLAG_COMMUNAL_HUB -import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR -import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.Flags.communalHub -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor @@ -58,11 +56,11 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha SceneContainerFlag.getMainStaticFlag() dependsOn MIGRATE_KEYGUARD_STATUS_BAR_VIEW // ComposeLockscreen dependencies - ComposeLockscreen.token dependsOn keyguardBottomAreaRefactor - ComposeLockscreen.token dependsOn migrateClocksToBlueprint + ComposeLockscreen.token dependsOn KeyguardBottomAreaRefactor.token + ComposeLockscreen.token dependsOn MigrateClocksToBlueprint.token // CommunalHub dependencies - communalHub dependsOn migrateClocksToBlueprint + communalHub dependsOn MigrateClocksToBlueprint.token } private inline val politeNotifications @@ -71,10 +69,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications()) private inline val vibrateWhileUnlockedToken: FlagToken get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked()) - private inline val keyguardBottomAreaRefactor - get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()) - private inline val migrateClocksToBlueprint - get() = FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint()) private inline val communalHub get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt index ec72a1422973..f1620d96b159 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/qs/QSLongPressEffect.kt @@ -214,6 +214,24 @@ class QSLongPressEffect( _actionType.value = null } + /** + * Reset the effect with a new effect duration. + * + * The effect will go back to an [IDLE] state where it can begin its logic with a new duration. + * + * @param[duration] New duration for the long-press effect + */ + fun resetWithDuration(duration: Int) { + // The effect can't reset if it is running + if (effectAnimator.isRunning) return + + effectAnimator.duration = duration.toLong() + _effectProgress.value = 0f + _actionType.value = null + waitJob?.cancel() + state = State.IDLE + } + enum class State { IDLE, /* The effect is idle waiting for touch input */ TIMEOUT_WAIT, /* The effect is waiting for a [PRESSED_TIMEOUT] period */ diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt new file mode 100644 index 000000000000..f49cfdda8b0a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/CommandLineKeyboardRepository.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.keyboard.data.repository + +import android.util.Log +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyboard.data.model.Keyboard +import com.android.systemui.keyboard.shared.model.BacklightModel +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull + +/** + * Helper class for development to mock various keyboard states with command line. Alternative for + * [KeyboardRepositoryImpl] which relies on real data from framework. [KeyboardRepositoryImpl] is + * the default implementation so to use this class you need to substitute it in [KeyboardModule]. + * + * For usage information: see [KeyboardCommand.help] or run `adb shell cmd statusbar keyboard`. + */ +@SysUISingleton +class CommandLineKeyboardRepository @Inject constructor(commandRegistry: CommandRegistry) : + KeyboardRepository { + + private val _isAnyKeyboardConnected = MutableStateFlow(false) + override val isAnyKeyboardConnected: Flow<Boolean> = _isAnyKeyboardConnected + + private val _backlightState: MutableStateFlow<BacklightModel?> = MutableStateFlow(null) + // filtering to make sure backlight doesn't have default initial value + override val backlight: Flow<BacklightModel> = _backlightState.filterNotNull() + + private val _newlyConnectedKeyboard: MutableStateFlow<Keyboard?> = MutableStateFlow(null) + override val newlyConnectedKeyboard: Flow<Keyboard> = _newlyConnectedKeyboard.filterNotNull() + + init { + Log.i(TAG, "initializing shell command $COMMAND") + commandRegistry.registerCommand(COMMAND) { KeyboardCommand() } + } + + inner class KeyboardCommand : Command { + override fun execute(pw: PrintWriter, args: List<String>) { + Log.i(TAG, "$COMMAND command was called with args: $args") + if (args.isEmpty()) { + help(pw) + return + } + when (args[0]) { + "keyboard-connected" -> _isAnyKeyboardConnected.value = args[1].toBoolean() + "backlight" -> { + @Suppress("Since15") + val level = Math.clamp(args[1].toInt().toLong(), 0, MAX_BACKLIGHT_LEVEL) + _backlightState.value = BacklightModel(level, MAX_BACKLIGHT_LEVEL) + } + "new-keyboard" -> { + _newlyConnectedKeyboard.value = + Keyboard(vendorId = args[1].toInt(), productId = args[2].toInt()) + } + else -> help(pw) + } + } + + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar $COMMAND <command>") + pw.println( + "Note: this command only mocks setting these values on the framework level" + + " but in reality doesn't change anything and is only used for testing UI" + ) + pw.println("Available commands:") + pw.println(" keyboard-connected [true|false]") + pw.println(" Notify any physical keyboard connected/disconnected.") + pw.println(" backlight <level>") + pw.println(" Notify new keyboard backlight level: min 0, max $MAX_BACKLIGHT_LEVEL.") + pw.println(" new-keyboard <vendor-id> <product-id>") + pw.println(" Notify new physical keyboard with specified parameters got connected.") + } + } + + companion object { + private const val TAG = "CommandLineKeyboardRepository" + private const val COMMAND = "keyboard" + private const val MAX_BACKLIGHT_LEVEL = 5 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt index 2fac40a48d3d..91d528074723 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/data/repository/KeyboardRepository.kt @@ -46,6 +46,10 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.shareIn +/** + * Provides information about physical keyboard states. [CommandLineKeyboardRepository] can be + * useful command line-driven implementation during development. + */ interface KeyboardRepository { /** Emits true if any physical keyboard is connected to the device, false otherwise. */ val isAnyKeyboardConnected: Flow<Boolean> diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt new file mode 100644 index 000000000000..779b27b25375 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the keyguard bottom area refactor flag. */ +@Suppress("NOTHING_TO_INLINE") +object KeyguardBottomAreaRefactor { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.keyguardBottomAreaRefactor() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 5565ee295786..d9d747015abd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -36,7 +36,6 @@ import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardStatusViewComponent import com.android.systemui.CoreStartable -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor @@ -166,7 +165,7 @@ constructor( fun bindIndicationArea() { indicationAreaHandle?.dispose() - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled) { keyguardRootView.findViewById<View?>(R.id.keyguard_indication_area)?.let { keyguardRootView.removeView(it) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 3b34750756b4..a293afcc28dc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -40,7 +40,6 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE; import static com.android.systemui.DejankUtils.whitelistIpcs; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.Flags.notifyPowerManagerUserActivityBackground; import static com.android.systemui.Flags.refactorGetCurrentUser; import static com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.DREAMING_ANIMATION_DURATION_MS; @@ -155,7 +154,7 @@ import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionStateManager; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -3404,7 +3403,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } // Ensure that keyguard becomes visible if the going away animation is canceled - if (showKeyguard && !KeyguardWmStateRefactor.isEnabled() && migrateClocksToBlueprint()) { + if (showKeyguard && !KeyguardWmStateRefactor.isEnabled() + && MigrateClocksToBlueprint.isEnabled()) { mKeyguardInteractor.showKeyguard(); } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt new file mode 100644 index 000000000000..5a2943bd00b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the migrate clocks to blueprint flag. */ +@Suppress("NOTHING_TO_INLINE") +object MigrateClocksToBlueprint { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.migrateClocksToBlueprint() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt index 7ad5aac63837..3f4d3a8544d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.data.repository import android.os.UserHandle import android.provider.Settings -import androidx.annotation.VisibleForTesting import com.android.keyguard.ClockEventController import com.android.keyguard.KeyguardClockSwitch.ClockSize import com.android.keyguard.KeyguardClockSwitch.LARGE @@ -52,14 +51,14 @@ interface KeyguardClockRepository { val clockSize: StateFlow<Int> /** clock size selected in picker, DYNAMIC or SMALL */ - val selectedClockSize: Flow<SettingsClockSize> + val selectedClockSize: StateFlow<SettingsClockSize> /** clock id, selected from clock carousel in wallpaper picker */ val currentClockId: Flow<ClockId> val currentClock: StateFlow<ClockController?> - val previewClockPair: StateFlow<Pair<ClockController, ClockController>> + val previewClock: Flow<ClockController> val clockEventController: ClockEventController fun setClockSize(@ClockSize size: Int) @@ -84,14 +83,19 @@ constructor( _clockSize.value = size } - override val selectedClockSize: Flow<SettingsClockSize> = + override val selectedClockSize: StateFlow<SettingsClockSize> = secureSettings .observerFlow( names = arrayOf(Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK), userId = UserHandle.USER_SYSTEM, ) .onStart { emit(Unit) } // Forces an initial update. - .map { getClockSize() } + .map { withContext(backgroundDispatcher) { getClockSize() } } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = getClockSize() + ) override val currentClockId: Flow<ClockId> = callbackFlow { @@ -113,37 +117,35 @@ constructor( override val currentClock: StateFlow<ClockController?> = currentClockId - .map { clockRegistry.createCurrentClock() } + .map { + clockEventController.clock = clockRegistry.createCurrentClock() + clockEventController.clock + } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = clockRegistry.createCurrentClock() ) - override val previewClockPair: StateFlow<Pair<ClockController, ClockController>> = - currentClockId - .map { Pair(clockRegistry.createCurrentClock(), clockRegistry.createCurrentClock()) } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = - Pair(clockRegistry.createCurrentClock(), clockRegistry.createCurrentClock()) - ) + override val previewClock: Flow<ClockController> = + currentClockId.map { + // We should create a new instance for each collect call + // cause in preview, the same clock will be attached to different view + // at the same time + clockRegistry.createCurrentClock() + } - @VisibleForTesting - suspend fun getClockSize(): SettingsClockSize { - return withContext(backgroundDispatcher) { - if ( - secureSettings.getIntForUser( - Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, - 1, - UserHandle.USER_CURRENT - ) == 1 - ) { - SettingsClockSize.DYNAMIC - } else { - SettingsClockSize.SMALL - } + private fun getClockSize(): SettingsClockSize { + return if ( + secureSettings.getIntForUser( + Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, + 1, + UserHandle.USER_CURRENT + ) == 1 + ) { + SettingsClockSize.DYNAMIC + } else { + SettingsClockSize.SMALL } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 9c68c45476d5..a36bf8bf8751 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -119,24 +119,7 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio init { // Seed with transitions signaling a boot into lockscreen state. If updating this, please // also update FakeKeyguardTransitionRepository. - emitTransition( - TransitionStep( - KeyguardState.OFF, - KeyguardState.LOCKSCREEN, - 0f, - TransitionState.STARTED, - KeyguardTransitionRepositoryImpl::class.simpleName!!, - ) - ) - emitTransition( - TransitionStep( - KeyguardState.OFF, - KeyguardState.LOCKSCREEN, - 1f, - TransitionState.FINISHED, - KeyguardTransitionRepositoryImpl::class.simpleName!!, - ) - ) + initialTransitionSteps.forEach(::emitTransition) } override fun startTransition(info: TransitionInfo): UUID? { @@ -256,5 +239,31 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio companion object { private const val TAG = "KeyguardTransitionRepository" + + /** + * Transition steps to seed the repository with, so that all of the transition interactor + * flows emit reasonable initial values. + */ + val initialTransitionSteps: List<TransitionStep> = + listOf( + TransitionStep( + KeyguardState.OFF, + KeyguardState.OFF, + 1f, + TransitionState.FINISHED, + ), + TransitionStep( + KeyguardState.OFF, + KeyguardState.LOCKSCREEN, + 0f, + TransitionState.STARTED, + ), + TransitionStep( + KeyguardState.OFF, + KeyguardState.LOCKSCREEN, + 1f, + TransitionState.FINISHED, + ), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 9040e031d54e..d09ee54f2029 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -252,5 +252,6 @@ constructor( val TO_LOCKSCREEN_DURATION = 500.milliseconds val TO_GONE_DURATION = DEFAULT_DURATION val TO_OCCLUDED_DURATION = DEFAULT_DURATION + val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 9a6088de110e..1f24fc23bbdd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -231,5 +231,7 @@ constructor( private val DEFAULT_DURATION = 500.milliseconds val TO_GLANCEABLE_HUB_DURATION = 1.seconds val TO_LOCKSCREEN_DURATION = 1167.milliseconds + val TO_AOD_DURATION = 300.milliseconds + val TO_GONE_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 12b27eb195fb..2649d4347495 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -289,7 +289,10 @@ constructor( .collect { pair -> val (isKeyguardGoingAway, lastStartedStep) = pair if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) { - startTransitionTo(KeyguardState.GONE) + startTransitionTo( + KeyguardState.GONE, + modeOnCanceled = TransitionModeOnCanceled.RESET, + ) } } } @@ -303,20 +306,6 @@ constructor( startTransitionTo(KeyguardState.GONE) } } - - return - } - - scope.launch { - keyguardInteractor.isKeyguardGoingAway - .sample(startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - KeyguardWmStateRefactor.assertInLegacyMode() - val (isKeyguardGoingAway, lastStartedStep) = pair - if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) { - startTransitionTo(KeyguardState.GONE) - } - } } } @@ -413,7 +402,7 @@ constructor( val TO_OCCLUDED_DURATION = 450.milliseconds val TO_AOD_DURATION = 500.milliseconds val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION - val TO_GONE_DURATION = DEFAULT_DURATION + val TO_GONE_DURATION = 633.milliseconds val TO_GLANCEABLE_HUB_DURATION = 1.seconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt index b9ec58ccb925..53f241684a62 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt @@ -39,7 +39,7 @@ constructor( /** The position of the keyguard clock. */ private val _clockPosition = MutableStateFlow(Position(0, 0)) /** See [ClockSection] */ - @Deprecated("with migrateClocksToBlueprint()") + @Deprecated("with MigrateClocksToBlueprint.isEnabled") val clockPosition: Flow<Position> = _clockPosition.asStateFlow() fun setClockPosition(x: Int, y: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt index 2cf91563b3e4..d492135bd482 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt @@ -38,14 +38,13 @@ constructor( private val keyguardClockRepository: KeyguardClockRepository, ) { - val selectedClockSize: Flow<SettingsClockSize> = keyguardClockRepository.selectedClockSize + val selectedClockSize: StateFlow<SettingsClockSize> = keyguardClockRepository.selectedClockSize val currentClockId: Flow<ClockId> = keyguardClockRepository.currentClockId val currentClock: StateFlow<ClockController?> = keyguardClockRepository.currentClock - val previewClockPair: StateFlow<Pair<ClockController, ClockController>> = - keyguardClockRepository.previewClockPair + val previewClock: Flow<ClockController> = keyguardClockRepository.previewClock var clock: ClockController? by keyguardClockRepository.clockEventController::clock diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index e6655ee3898f..0cd7d18b2342 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -91,11 +91,46 @@ constructor( } } + val transitions = repository.transitions + + /** + * A pair of the most recent STARTED step, and the transition step immediately preceding it. The + * transition framework enforces that the previous step is either a CANCELED or FINISHED step, + * and that the previous step was *to* the state the STARTED step is *from*. + * + * This flow can be used to access the previous step to determine whether it was CANCELED or + * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming + * from when we were canceled. + */ + val startedStepWithPrecedingStep = + transitions + .pairwise() + .filter { it.newValue.transitionState == TransitionState.STARTED } + .shareIn(scope, SharingStarted.Eagerly) + init { + // Collect non-canceled steps and emit transition values. scope.launch(mainDispatcher) { - repository.transitions.collect { step -> - getTransitionValueFlow(step.from).emit(1f - step.value) - getTransitionValueFlow(step.to).emit(step.value) + repository.transitions + .filter { it.transitionState != TransitionState.CANCELED } + .collect { step -> + getTransitionValueFlow(step.from).emit(1f - step.value) + getTransitionValueFlow(step.to).emit(step.value) + } + } + + // If a transition from state A -> B is canceled in favor of a transition from B -> C, we + // need to ensure we emit transitionValue(A) = 0f, since no further steps will be emitted + // where the from or to states are A. This would leave transitionValue(A) stuck at an + // arbitrary non-zero value. + scope.launch(mainDispatcher) { + startedStepWithPrecedingStep.collect { (prevStep, startedStep) -> + if ( + prevStep.transitionState == TransitionState.CANCELED && + startedStep.to != prevStep.from + ) { + getTransitionValueFlow(prevStep.from).emit(0f) + } } } } @@ -202,8 +237,6 @@ constructor( val dozingToLockscreenTransition: Flow<TransitionStep> = repository.transition(DOZING, LOCKSCREEN) - val transitions = repository.transitions - /** Receive all [TransitionStep] matching a filter of [from]->[to] */ fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> { return repository.transition(from, to) @@ -250,21 +283,6 @@ constructor( .stateIn(scope, SharingStarted.Eagerly, DOZING) /** - * A pair of the most recent STARTED step, and the transition step immediately preceding it. The - * transition framework enforces that the previous step is either a CANCELED or FINISHED step, - * and that the previous step was *to* the state the STARTED step is *from*. - * - * This flow can be used to access the previous step to determine whether it was CANCELED or - * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming - * from when we were canceled. - */ - val startedStepWithPrecedingStep = - transitions - .pairwise() - .filter { it.newValue.transitionState == TransitionState.STARTED } - .stateIn(scope, SharingStarted.Eagerly, null) - - /** * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition. * * WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt index b8ba09801ee8..5de1a61d61b5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -17,10 +17,10 @@ package com.android.systemui.keyguard.ui import android.view.animation.Interpolator import com.android.app.animation.Interpolators.LINEAR -import com.android.app.tracing.coroutines.launch import com.android.keyguard.logging.KeyguardTransitionAnimationLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState @@ -35,6 +35,7 @@ import kotlin.math.max import kotlin.math.min import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow @@ -42,6 +43,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch /** * Assists in creating sub-flows for a KeyguardTransition. Call [setup] once for a transition, and @@ -52,13 +54,14 @@ class KeyguardTransitionAnimationFlow @Inject constructor( @Application private val scope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, private val transitionInteractor: KeyguardTransitionInteractor, private val logger: KeyguardTransitionAnimationLogger, ) { private val transitionMap = mutableMapOf<Edge, MutableSharedFlow<TransitionStep>>() init { - scope.launch("KeyguardTransitionAnimationFlow") { + scope.launch(mainDispatcher) { transitionInteractor.transitions.collect { // FROM->TO transitionMap[Edge(it.from, it.to)]?.emit(it) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index 4812e03ec3f6..7e3ddf92c530 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -26,9 +26,9 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config @@ -105,7 +105,7 @@ constructor( var transition = if ( - !keyguardBottomAreaRefactor() && + !KeyguardBottomAreaRefactor.isEnabled && prevBluePrint != null && prevBluePrint != blueprint ) { @@ -213,9 +213,10 @@ constructor( cs: ConstraintSet, viewModel: KeyguardClockViewModel ) { - if (!DEBUG || viewModel.clock == null) return + val currentClock = viewModel.currentClock.value + if (!DEBUG || currentClock == null) return val smallClockViewId = R.id.lockscreen_clock_view - val largeClockViewId = viewModel.clock!!.largeClock.layout.views[0].id + val largeClockViewId = currentClock.largeClock.layout.views[0].id Log.i( TAG, "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 01596ed2e3ef..6255f0d44609 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.binder import android.transition.TransitionManager import android.transition.TransitionSet import android.view.View.INVISIBLE +import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout @@ -27,7 +28,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.keyguard.KeyguardClockSwitch.LARGE import com.android.keyguard.KeyguardClockSwitch.SMALL -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type @@ -40,7 +41,8 @@ import kotlinx.coroutines.launch object KeyguardClockViewBinder { private val TAG = KeyguardClockViewBinder::class.simpleName!! - + // When changing to new clock, we need to remove old clock views from burnInLayer + private var lastClock: ClockController? = null @JvmStatic fun bind( clockSection: ClockSection, @@ -55,28 +57,27 @@ object KeyguardClockViewBinder { } } keyguardRootView.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.currentClock.collect { currentClock -> - cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer) - viewModel.clock = currentClock + cleanupClockViews(currentClock, keyguardRootView, viewModel.burnInLayer) addClockViews(currentClock, keyguardRootView) updateBurnInLayer(keyguardRootView, viewModel) applyConstraints(clockSection, keyguardRootView, true) } } launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.clockSize.collect { updateBurnInLayer(keyguardRootView, viewModel) blueprintInteractor.refreshBlueprint(Type.ClockSize) } } launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.clockShouldBeCentered.collect { clockShouldBeCentered -> - viewModel.clock?.let { + viewModel.currentClock.value?.let { // Weather clock also has hasCustomPositionUpdatedAnimation as true // TODO(b/323020908): remove ID check if ( @@ -91,9 +92,9 @@ object KeyguardClockViewBinder { } } launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.isAodIconsVisible.collect { isAodIconsVisible -> - viewModel.clock?.let { + viewModel.currentClock.value?.let { // Weather clock also has hasCustomPositionUpdatedAnimation as true if ( viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER" @@ -132,11 +133,14 @@ object KeyguardClockViewBinder { } private fun cleanupClockViews( - clockController: ClockController?, + currentClock: ClockController?, rootView: ConstraintLayout, burnInLayer: Layer? ) { - clockController?.let { clock -> + if (lastClock == currentClock) { + return + } + lastClock?.let { clock -> clock.smallClock.layout.views.forEach { burnInLayer?.removeView(it) rootView.removeView(it) @@ -150,6 +154,7 @@ object KeyguardClockViewBinder { } clock.largeClock.layout.views.forEach { rootView.removeView(it) } } + lastClock = currentClock } @VisibleForTesting @@ -157,11 +162,19 @@ object KeyguardClockViewBinder { clockController: ClockController?, rootView: ConstraintLayout, ) { + // We'll collect the same clock when exiting wallpaper picker without changing clock + // so we need to remove clock views from parent before addView again clockController?.let { clock -> clock.smallClock.layout.views.forEach { + if (it.parent != null) { + (it.parent as ViewGroup).removeView(it) + } rootView.addView(it).apply { it.visibility = INVISIBLE } } clock.largeClock.layout.views.forEach { + if (it.parent != null) { + (it.parent as ViewGroup).removeView(it) + } rootView.addView(it).apply { it.visibility = INVISIBLE } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index 841f52d7aa64..267d68e5e5e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -22,8 +22,8 @@ import android.view.ViewGroup import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R @@ -69,7 +69,10 @@ object KeyguardIndicationAreaBinder { launch { // Do not independently apply alpha, as [KeyguardRootViewModel] should work // for this and all its children - if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) { + if ( + !(MigrateClocksToBlueprint.isEnabled || + KeyguardBottomAreaRefactor.isEnabled) + ) { viewModel.alpha.collect { alpha -> view.alpha = alpha } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt index 46c354a45c92..d9f12c34c4f1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt @@ -32,7 +32,6 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.keyguard.ClockEventController import com.android.systemui.customization.R as customizationR import com.android.systemui.keyguard.shared.model.SettingsClockSize import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer @@ -44,12 +43,10 @@ import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R import com.android.systemui.util.Utils import kotlin.reflect.KSuspendFunction1 -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch /** Binder for the small clock view, large clock view. */ object KeyguardPreviewClockViewBinder { - @JvmStatic fun bind( largeClockHostView: View, @@ -72,52 +69,38 @@ object KeyguardPreviewClockViewBinder { @JvmStatic fun bind( context: Context, - displayId: Int, rootView: ConstraintLayout, viewModel: KeyguardPreviewClockViewModel, - clockEventController: ClockEventController, updateClockAppearance: KSuspendFunction1<ClockController, Unit>, ) { - // TODO(b/327668072): When this function is called multiple times, the clock view can be - // gone due to a race condition on removeView and addView. rootView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { - combine(viewModel.selectedClockSize, viewModel.previewClockPair) { _, clock -> - clock + var lastClock: ClockController? = null + viewModel.previewClock.collect { currentClock -> + lastClock?.let { clock -> + (clock.largeClock.layout.views + clock.smallClock.layout.views) + .forEach { rootView.removeView(it) } } - .collect { previewClockPair -> - viewModel.lastClockPair?.let { clockPair -> - (clockPair.first.largeClock.layout.views + - clockPair.first.smallClock.layout.views) - .forEach { rootView.removeView(it) } - (clockPair.second.largeClock.layout.views + - clockPair.second.smallClock.layout.views) - .forEach { rootView.removeView(it) } - } - viewModel.lastClockPair = previewClockPair - val clockPreview = - if (displayId == 0) previewClockPair.first - else previewClockPair.second - clockEventController.clock = clockPreview - updateClockAppearance(clockPreview) + lastClock = currentClock + updateClockAppearance(currentClock) - if (viewModel.shouldHighlightSelectedAffordance) { - (clockPreview.largeClock.layout.views + - clockPreview.smallClock.layout.views) - .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA } - } - clockPreview.largeClock.layout.views.forEach { - (it.parent as? ViewGroup)?.removeView(it) - rootView.addView(it) - } + if (viewModel.shouldHighlightSelectedAffordance) { + (currentClock.largeClock.layout.views + + currentClock.smallClock.layout.views) + .forEach { it.alpha = KeyguardPreviewRenderer.DIM_ALPHA } + } + currentClock.largeClock.layout.views.forEach { + (it.parent as? ViewGroup)?.removeView(it) + rootView.addView(it) + } - clockPreview.smallClock.layout.views.forEach { - (it.parent as? ViewGroup)?.removeView(it) - rootView.addView(it) - } - applyPreviewConstraints(context, rootView, viewModel) + currentClock.smallClock.layout.views.forEach { + (it.parent as? ViewGroup)?.removeView(it) + rootView.addView(it) } + applyPreviewConstraints(context, rootView, currentClock, viewModel) + } } } } @@ -170,15 +153,13 @@ object KeyguardPreviewClockViewBinder { private fun applyPreviewConstraints( context: Context, rootView: ConstraintLayout, + previewClock: ClockController, viewModel: KeyguardPreviewClockViewModel ) { val cs = ConstraintSet().apply { clone(rootView) } - val clockPair = viewModel.previewClockPair.value applyClockDefaultConstraints(context, cs) - clockPair.first.largeClock.layout.applyPreviewConstraints(cs) - clockPair.first.smallClock.layout.applyPreviewConstraints(cs) - clockPair.second.largeClock.layout.applyPreviewConstraints(cs) - clockPair.second.smallClock.layout.applyPreviewConstraints(cs) + previewClock.largeClock.layout.applyPreviewConstraints(cs) + previewClock.smallClock.layout.applyPreviewConstraints(cs) // When selectedClockSize is the initial value, make both clocks invisible to avoid // flickering @@ -194,12 +175,9 @@ object KeyguardPreviewClockViewBinder { SettingsClockSize.SMALL -> VISIBLE null -> INVISIBLE } - cs.apply { - setVisibility(clockPair.first.largeClock.layout.views, largeClockVisibility) - setVisibility(clockPair.first.smallClock.layout.views, smallClockVisibility) - setVisibility(clockPair.second.largeClock.layout.views, largeClockVisibility) - setVisibility(clockPair.second.smallClock.layout.views, smallClockVisibility) + setVisibility(previewClock.largeClock.layout.views, largeClockVisibility) + setVisibility(previewClock.smallClock.layout.views, smallClockVisibility) } cs.applyTo(rootView) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index d0246a8cd872..0ed42ef75026 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -36,8 +36,6 @@ import com.android.app.animation.Interpolators import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.keyguard.KeyguardClockSwitch.MISSING_CLOCK_ID -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.Flags.newAodTransition import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text @@ -45,6 +43,8 @@ import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters @@ -109,7 +109,7 @@ object KeyguardRootViewBinder { val endButton = R.id.end_button val lockIcon = R.id.lock_icon_view - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { view.setOnTouchListener { _, event -> if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) { viewModel.setRootViewLastTapPosition(Point(event.x.toInt(), event.y.toInt())) @@ -143,11 +143,13 @@ object KeyguardRootViewBinder { } } - if (keyguardBottomAreaRefactor() || DeviceEntryUdfpsRefactor.isEnabled) { + if ( + KeyguardBottomAreaRefactor.isEnabled || DeviceEntryUdfpsRefactor.isEnabled + ) { launch { viewModel.alpha(viewState).collect { alpha -> view.alpha = alpha - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { childViews[statusViewId]?.alpha = alpha childViews[burnInLayerId]?.alpha = alpha } @@ -155,7 +157,7 @@ object KeyguardRootViewBinder { } } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { launch { viewModel.burnInLayerVisibility.collect { visibility -> childViews[burnInLayerId]?.visibility = visibility @@ -342,13 +344,13 @@ object KeyguardRootViewBinder { } } - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { burnInParams.update { current -> current.copy(clockControllerProvider = clockControllerProvider) } } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { burnInParams.update { current -> current.copy(translationY = { childViews[burnInLayerId]?.translationY }) } @@ -439,7 +441,7 @@ object KeyguardRootViewBinder { burnInParams.update { current -> current.copy( minViewY = - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { // To ensure burn-in doesn't enroach the top inset, get the min top Y childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) -> min( @@ -472,7 +474,7 @@ object KeyguardRootViewBinder { configuration: ConfigurationState, screenOffAnimationController: ScreenOffAnimationController, ) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { throw IllegalStateException("should only be called in legacy code paths") } if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return @@ -503,7 +505,7 @@ object KeyguardRootViewBinder { } when { !isVisible.isAnimating -> { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { translationY = 0f } visibility = @@ -553,7 +555,7 @@ object KeyguardRootViewBinder { animatorListener: Animator.AnimatorListener, ) { if (animate) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { translationY = -iconAppearTranslation.toFloat() } alpha = 0f @@ -561,19 +563,19 @@ object KeyguardRootViewBinder { .alpha(1f) .setInterpolator(Interpolators.LINEAR) .setDuration(AOD_ICONS_APPEAR_DURATION) - .apply { if (migrateClocksToBlueprint()) animateInIconTranslation() } + .apply { if (MigrateClocksToBlueprint.isEnabled) animateInIconTranslation() } .setListener(animatorListener) .start() } else { alpha = 1.0f - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { translationY = 0f } } } private fun View.animateInIconTranslation() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start() } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index b77f0c5a1e60..9aebf66aa067 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -21,7 +21,7 @@ import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type @@ -41,9 +41,9 @@ object KeyguardSmartspaceViewBinder { blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay -> updateDateWeatherToBurnInLayer( @@ -62,7 +62,7 @@ object KeyguardSmartspaceViewBinder { } launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch smartspaceViewModel.bcSmartspaceVisibility.collect { updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel) blueprintInteractor.refreshBlueprint( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 7c76e6afc074..14ab17f9641b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -50,8 +50,6 @@ import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import androidx.core.view.isInvisible import com.android.keyguard.ClockEventController import com.android.keyguard.KeyguardClockSwitch -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher @@ -61,6 +59,8 @@ import com.android.systemui.communal.ui.viewmodel.CommunalTutorialIndicatorViewM import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder @@ -90,6 +90,7 @@ import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.statusbar.phone.KeyguardBottomAreaView import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.util.kotlin.DisposableHandles import com.android.systemui.util.settings.SecureSettings import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -173,7 +174,7 @@ constructor( private lateinit var smallClockHostView: FrameLayout private var smartSpaceView: View? = null - private val disposables = mutableSetOf<DisposableHandle>() + private val disposables = DisposableHandles() private var isDestroyed = false private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>() @@ -183,9 +184,9 @@ constructor( init { coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job()) - disposables.add(DisposableHandle { coroutineScope.cancel() }) + disposables += DisposableHandle { coroutineScope.cancel() } - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { quickAffordancesCombinedViewModel.enablePreviewMode( initiallySelectedSlotId = bundle.getString( @@ -203,7 +204,7 @@ constructor( shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance, ) } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance } runBlocking(mainDispatcher) { @@ -214,7 +215,7 @@ constructor( if (hostToken == null) null else InputTransferToken(hostToken), "KeyguardPreviewRenderer" ) - disposables.add(DisposableHandle { host.release() }) + disposables += DisposableHandle { host.release() } } } @@ -230,7 +231,7 @@ constructor( setupKeyguardRootView(previewContext, rootView) - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled) { setUpBottomArea(rootView) } @@ -274,7 +275,7 @@ constructor( } fun onSlotSelected(slotId: String) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId) } else { bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId) @@ -284,8 +285,8 @@ constructor( fun destroy() { isDestroyed = true lockscreenSmartspaceController.disconnect() - disposables.forEach { it.dispose() } - if (keyguardBottomAreaRefactor()) { + disposables.dispose() + if (KeyguardBottomAreaRefactor.isEnabled) { shortcutsBindings.forEach { it.destroy() } } } @@ -371,8 +372,8 @@ constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) { val keyguardRootView = KeyguardRootView(previewContext, null) - if (!keyguardBottomAreaRefactor()) { - disposables.add( + if (!KeyguardBottomAreaRefactor.isEnabled) { + disposables += KeyguardRootViewBinder.bind( keyguardRootView, keyguardRootViewModel, @@ -387,7 +388,6 @@ constructor( null, // device entry haptics not required for preview mode null, // falsing manager not required for preview mode ) - ) } rootView.addView( keyguardRootView, @@ -397,21 +397,22 @@ constructor( ), ) - setUpUdfps(previewContext, if (migrateClocksToBlueprint()) keyguardRootView else rootView) + setUpUdfps( + previewContext, + if (MigrateClocksToBlueprint.isEnabled) keyguardRootView else rootView + ) - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { setupShortcuts(keyguardRootView) } if (!shouldHideClock) { setUpClock(previewContext, rootView) - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { KeyguardPreviewClockViewBinder.bind( context, - displayId, keyguardRootView, clockViewModel, - clockController, ::updateClockAppearance ) } else { @@ -482,7 +483,7 @@ constructor( ) as View // Place the UDFPS view in the proper sensor location - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { finger.id = R.id.lock_icon_view parentView.addView(finger) val cs = ConstraintSet() @@ -509,7 +510,7 @@ constructor( private fun setUpClock(previewContext: Context, parentView: ViewGroup) { val resources = parentView.resources - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { largeClockHostView = FrameLayout(previewContext) largeClockHostView.layoutParams = FrameLayout.LayoutParams( @@ -547,7 +548,7 @@ constructor( } // TODO (b/283465254): Move the listeners to KeyguardClockRepository - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { val clockChangeListener = object : ClockRegistry.ClockChangeListener { override fun onCurrentClockChanged() { @@ -555,14 +556,12 @@ constructor( } } clockRegistry.registerClockChangeListener(clockChangeListener) - disposables.add( - DisposableHandle { - clockRegistry.unregisterClockChangeListener(clockChangeListener) - } - ) + disposables += DisposableHandle { + clockRegistry.unregisterClockChangeListener(clockChangeListener) + } clockController.registerListeners(parentView) - disposables.add(DisposableHandle { clockController.unregisterListeners() }) + disposables += DisposableHandle { clockController.unregisterListeners() } } val receiver = @@ -581,9 +580,9 @@ constructor( addAction(Intent.ACTION_TIME_CHANGED) }, ) - disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }) + disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) } - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { val layoutChangeListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> if (clockController.clock !is DefaultClockController) { @@ -602,9 +601,9 @@ constructor( } } parentView.addOnLayoutChangeListener(layoutChangeListener) - disposables.add( - DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) } - ) + disposables += DisposableHandle { + parentView.removeOnLayoutChangeListener(layoutChangeListener) + } } onClockChanged() @@ -631,7 +630,7 @@ constructor( } } private fun onClockChanged() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { return } coroutineScope.launch { @@ -678,7 +677,7 @@ constructor( } private fun updateLargeClock(clock: ClockController) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { return } clock.largeClock.events.onTargetRegionChanged( @@ -692,7 +691,7 @@ constructor( } private fun updateSmallClock(clock: ClockController) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { return } clock.smallClock.events.onTargetRegionChanged( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt index f20c4acba448..3b21141273e0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt @@ -22,10 +22,12 @@ import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBounc import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DreamingToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel @@ -89,6 +91,12 @@ abstract class DeviceEntryIconTransitionModule { @Binds @IntoSet + abstract fun aodToPrimaryBouncer( + impl: AodToPrimaryBouncerTransitionViewModel + ): DeviceEntryIconTransition + + @Binds + @IntoSet abstract fun dozingToGone(impl: DozingToGoneTransitionViewModel): DeviceEntryIconTransition @Binds @@ -111,6 +119,10 @@ abstract class DeviceEntryIconTransitionModule { @Binds @IntoSet + abstract fun dreamingToAod(impl: DreamingToAodTransitionViewModel): DeviceEntryIconTransition + + @Binds + @IntoSet abstract fun dreamingToLockscreen( impl: DreamingToLockscreenTransitionViewModel ): DeviceEntryIconTransition diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt index 9c9df806c38c..a215efa724f9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt @@ -41,7 +41,7 @@ class BaseBlueprintTransition(val clockViewModel: KeyguardClockViewModel) : Tran private fun excludeClockAndSmartspaceViews(transition: Transition) { transition.excludeTarget(SmartspaceView::class.java, true) - clockViewModel.clock?.let { clock -> + clockViewModel.currentClock.value?.let { clock -> clock.largeClock.layout.views.forEach { view -> transition.excludeTarget(view, true) } clock.smallClock.layout.views.forEach { view -> transition.excludeTarget(view, true) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt index 3adeb2aeb283..c69d868866d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt @@ -57,7 +57,9 @@ class IntraBlueprintTransition( when (config.type) { Type.NoTransition -> {} Type.DefaultClockStepping -> - addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) }) + addTransition( + clockViewModel.currentClock.value?.let { DefaultClockSteppingTransition(it) } + ) else -> addTransition(ClockSizeTransition(config, clockViewModel, smartspaceViewModel)) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt index cd46d6cf2188..2e9663897f89 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt @@ -25,9 +25,9 @@ import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel @@ -49,14 +49,14 @@ constructor( private val vibratorHelper: VibratorHelper, ) : BaseShortcutSection() { override fun addViews(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { addLeftShortcut(constraintLayout) addRightShortcut(constraintLayout) } } override fun bindData(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { leftShortcutHandle = KeyguardQuickAffordanceViewBinder.bind( constraintLayout.requireViewById(R.id.start_button), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt index 88ce9dc88a7b..d639978764f8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt @@ -23,7 +23,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel @@ -47,7 +47,7 @@ constructor( } } override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } @@ -62,14 +62,14 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } clockViewModel.burnInLayer = burnInLayer } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index 3d9c04e39679..2832e9d8a35e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -26,8 +26,8 @@ import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore @@ -58,7 +58,7 @@ constructor( private lateinit var nic: NotificationIconContainer override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } nic = @@ -77,7 +77,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } @@ -98,7 +98,7 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } val bottomMargin = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index a183b720c087..881467ff2724 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -30,8 +30,8 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.constraintlayout.widget.ConstraintSet.VISIBLE import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT -import com.android.systemui.Flags import com.android.systemui.customization.R as customizationR +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection @@ -70,7 +70,7 @@ constructor( override fun addViews(constraintLayout: ConstraintLayout) {} override fun bindData(constraintLayout: ConstraintLayout) { - if (!Flags.migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } KeyguardClockViewBinder.bind( @@ -83,10 +83,10 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!Flags.migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } - clockInteractor.clock?.let { clock -> + keyguardClockViewModel.currentClock.value?.let { clock -> constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet)) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt index 8fd8becab76f..4c846e424f4b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt @@ -28,13 +28,12 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder import com.android.systemui.keyguard.ui.view.DeviceEntryIconView @@ -72,8 +71,8 @@ constructor( override fun addViews(constraintLayout: ConstraintLayout) { if ( - !keyguardBottomAreaRefactor() && - !migrateClocksToBlueprint() && + !KeyguardBottomAreaRefactor.isEnabled && + !DeviceEntryUdfpsRefactor.isEnabled && !DeviceEntryUdfpsRefactor.isEnabled ) { return @@ -87,7 +86,7 @@ constructor( if (DeviceEntryUdfpsRefactor.isEnabled) { DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId } } else { - // keyguardBottomAreaRefactor() or migrateClocksToBlueprint() + // KeyguardBottomAreaRefactor.isEnabled or MigrateClocksToBlueprint.isEnabled LockIconView(context, null).apply { id = R.id.lock_icon_view } } constraintLayout.addView(view) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt index 3361343423a9..af0528a4c354 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt @@ -21,7 +21,7 @@ import android.content.Context import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.Flags.keyguardBottomAreaRefactor +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea @@ -42,14 +42,14 @@ constructor( private var indicationAreaHandle: DisposableHandle? = null override fun addViews(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { val view = KeyguardIndicationArea(context, null) constraintLayout.addView(view) } } override fun bindData(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { indicationAreaHandle = KeyguardIndicationAreaBinder.bind( constraintLayout.requireViewById(R.id.keyguard_indication_area), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index 6a3b920f9692..380e361eb33e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -25,58 +25,42 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.centralizedStatusBarHeightFix -import com.android.systemui.Flags.migrateClocksToBlueprint -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.res.R -import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import dagger.Lazy import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher /** Single column format for notifications (default for phones) */ class DefaultNotificationStackScrollLayoutSection @Inject constructor( context: Context, - sceneContainerFlags: SceneContainerFlags, notificationPanelView: NotificationPanelView, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, - @Main mainDispatcher: CoroutineDispatcher, ) : NotificationStackScrollLayoutSection( context, - sceneContainerFlags, notificationPanelView, sharedNotificationContainer, sharedNotificationContainerViewModel, - notificationStackAppearanceViewModel, - ambientState, - controller, - notificationStackSizeCalculator, - mainDispatcher, + sharedNotificationContainerBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } constraintSet.apply { val bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin) - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { val useLargeScreenHeader = context.resources.getBoolean(R.bool.config_use_large_screen_shade_header) val marginTopLargeScreen = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt index a203c53be01e..32e76d0b24ff 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt @@ -29,9 +29,9 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import androidx.core.view.isVisible -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.animation.view.LaunchableLinearLayout import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardSettingsViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel @@ -56,7 +56,7 @@ constructor( private var settingsPopupMenuHandle: DisposableHandle? = null override fun addViews(constraintLayout: ConstraintLayout) { - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled) { return } val view = @@ -71,7 +71,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { settingsPopupMenuHandle = KeyguardSettingsViewBinder.bind( constraintLayout.requireViewById<View>(R.id.keyguard_settings_button), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt index 0c0eb8a673a4..45b82576c6c4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt @@ -25,8 +25,8 @@ import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel @@ -48,14 +48,14 @@ constructor( private val vibratorHelper: VibratorHelper, ) : BaseShortcutSection() { override fun addViews(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { addLeftShortcut(constraintLayout) addRightShortcut(constraintLayout) } } override fun bindData(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { leftShortcutHandle = KeyguardQuickAffordanceViewBinder.bind( constraintLayout.requireViewById(R.id.start_button), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt index 6e8605bde864..45641dbfc517 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt @@ -31,8 +31,8 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.keyguard.KeyguardStatusView import com.android.keyguard.dagger.KeyguardStatusViewComponent -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.KeyguardViewConfigurator +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.media.controls.ui.controller.KeyguardMediaController import com.android.systemui.res.R @@ -58,7 +58,7 @@ constructor( private val statusViewId = R.id.keyguard_status_view override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } // At startup, 2 views with the ID `R.id.keyguard_status_view` will be available. @@ -83,7 +83,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { constraintLayout.findViewById<KeyguardStatusView?>(R.id.keyguard_status_view)?.let { val statusViewComponent = keyguardStatusViewComponentFactory.build(it, context.display) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt index 3265d796ecc7..2abb7ba37340 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt @@ -20,10 +20,10 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.Flags import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import javax.inject.Inject @@ -66,7 +66,7 @@ constructor( ConstraintSet.BOTTOM, ) - if (Flags.keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { connect( viewId, ConstraintSet.BOTTOM, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt index d572c51d1146..a17c5e538382 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt @@ -22,7 +22,7 @@ import android.view.ViewGroup import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController @@ -34,7 +34,7 @@ constructor( val smartspaceController: LockscreenSmartspaceController, ) : KeyguardSection() { override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (smartspaceController.isEnabled()) return constraintLayout.findViewById<View?>(R.id.keyguard_slice_view)?.let { @@ -46,7 +46,7 @@ constructor( override fun bindData(constraintLayout: ConstraintLayout) {} override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (smartspaceController.isEnabled()) return constraintSet.apply { @@ -81,7 +81,7 @@ constructor( } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (smartspaceController.isEnabled()) return constraintLayout.removeView(R.id.keyguard_slice_view) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt index 5dea7cbb801d..2b601cdc012f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt @@ -25,38 +25,26 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R -import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle abstract class NotificationStackScrollLayoutSection constructor( protected val context: Context, - private val sceneContainerFlags: SceneContainerFlags, private val notificationPanelView: NotificationPanelView, private val sharedNotificationContainer: SharedNotificationContainer, private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - private val notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - private val ambientState: AmbientState, - private val controller: NotificationStackScrollLayoutController, - private val notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val mainDispatcher: CoroutineDispatcher, + private val sharedNotificationContainerBinder: SharedNotificationContainerBinder, ) : KeyguardSection() { private val placeHolderId = R.id.nssl_placeholder - private val disposableHandles: MutableList<DisposableHandle> = mutableListOf() + private var disposableHandle: DisposableHandle? = null /** * Align the notification placeholder bottom to the top of either the lock icon or the ambient @@ -82,7 +70,7 @@ constructor( } override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } // This moves the existing NSSL view to a different parent, as the controller is a @@ -98,43 +86,21 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } - disposeHandles() - disposableHandles.add( - SharedNotificationContainerBinder.bind( + disposableHandle?.dispose() + disposableHandle = + sharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainImmediateDispatcher = mainDispatcher, ) - ) - - if (sceneContainerFlags.isEnabled()) { - disposableHandles.add( - NotificationStackAppearanceViewBinder.bind( - context, - sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, - controller, - mainImmediateDispatcher = mainDispatcher, - ) - ) - } } override fun removeViews(constraintLayout: ConstraintLayout) { - disposeHandles() + disposableHandle?.dispose() + disposableHandle = null constraintLayout.removeView(placeHolderId) } - - private fun disposeHandles() { - disposableHandles.forEach { it.dispose() } - disposableHandles.clear() - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index b0f7a258a4e6..1847d2794787 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -23,8 +23,8 @@ import android.view.ViewTreeObserver.OnGlobalLayoutListener import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection @@ -56,7 +56,7 @@ constructor( private var pastVisibility: Int = -1 override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return smartspaceView = smartspaceController.buildAndConnectView(constraintLayout) weatherView = smartspaceController.buildAndConnectWeatherView(constraintLayout) @@ -83,7 +83,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return KeyguardSmartspaceViewBinder.bind( constraintLayout, @@ -94,7 +94,7 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return val horizontalPaddingStart = context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + @@ -191,7 +191,7 @@ constructor( } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return listOf(smartspaceView, dateView, weatherView).forEach { it?.let { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt index 21e945582aff..5dbba75411a5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt @@ -28,7 +28,7 @@ import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.media.controls.ui.controller.KeyguardMediaController import com.android.systemui.res.R @@ -46,7 +46,7 @@ constructor( private val mediaContainerId = R.id.status_view_media_container override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } @@ -73,7 +73,7 @@ constructor( override fun bindData(constraintLayout: ConstraintLayout) {} override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } @@ -87,7 +87,7 @@ constructor( } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt index 2545302ccaa1..1a7386678e14 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt @@ -23,51 +23,33 @@ import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.Flags.migrateClocksToBlueprint -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.res.R -import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher /** Large-screen format for notifications, shown as two columns on the device */ class SplitShadeNotificationStackScrollLayoutSection @Inject constructor( context: Context, - sceneContainerFlags: SceneContainerFlags, notificationPanelView: NotificationPanelView, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, - @Main mainDispatcher: CoroutineDispatcher, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, ) : NotificationStackScrollLayoutSection( context, - sceneContainerFlags, notificationPanelView, sharedNotificationContainer, sharedNotificationContainerViewModel, - notificationStackAppearanceViewModel, - ambientState, - controller, - notificationStackSizeCalculator, - mainDispatcher, + sharedNotificationContainerBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } constraintSet.apply { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index 6184c82cbff7..4d3a78d32b3a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -216,7 +216,9 @@ class ClockSizeTransition( captureSmartspace = !viewModel.useLargeClock && smartspaceViewModel.isSmartspaceEnabled if (viewModel.useLargeClock) { - viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } + viewModel.currentClock.value?.let { + it.largeClock.layout.views.forEach { addTarget(it) } + } } else { addTarget(R.id.lockscreen_clock_view) } @@ -276,7 +278,9 @@ class ClockSizeTransition( if (viewModel.useLargeClock) { addTarget(R.id.lockscreen_clock_view) } else { - viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } + viewModel.currentClock.value?.let { + it.largeClock.layout.views.forEach { addTarget(it) } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt index d26356ebc92b..ac2713d88f39 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.util.MathUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor.Companion.TO_GONE_DURATION import com.android.systemui.keyguard.shared.model.KeyguardState @@ -47,13 +48,16 @@ constructor( to = KeyguardState.GONE, ) - val lockscreenAlpha: Flow<Float> = - transitionAnimation.sharedFlow( + fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> { + var startAlpha = 1f + return transitionAnimation.sharedFlow( duration = 200.milliseconds, - onStep = { 1 - it }, + onStart = { startAlpha = viewState.alpha() }, + onStep = { MathUtils.lerp(startAlpha, 0f, it) }, onFinish = { 0f }, - onCancel = { 1f }, + onCancel = { startAlpha }, ) + } /** Scrim alpha values */ val scrimAlpha: Flow<ScrimAlpha> = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt index 5741b9485287..1e5f5a70bac8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt @@ -18,8 +18,8 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.AOD @@ -60,7 +60,7 @@ constructor( emit(goneToAodAlpha) } else if (step.from == GONE && step.to == DOZING) { emit(goneToDozingAlpha) - } else if (!migrateClocksToBlueprint()) { + } else if (!MigrateClocksToBlueprint.isEnabled) { emit(keyguardAlpha) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt index f961e083e64f..20549328838f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt @@ -22,9 +22,9 @@ import android.util.Log import android.util.MathUtils import com.android.app.animation.Interpolators import com.android.keyguard.KeyguardClockSwitch -import com.android.systemui.Flags import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -145,7 +145,7 @@ constructor( // Ensure the desired translation doesn't encroach on the top inset val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt() val translationY = - if (Flags.migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { max(params.topInset - params.minViewY, burnInY) } else { max(params.topInset, params.minViewY + burnInY) - params.minViewY @@ -168,8 +168,8 @@ constructor( private fun clockController( provider: Provider<ClockController>?, ): Provider<ClockController>? { - return if (Flags.migrateClocksToBlueprint()) { - Provider { keyguardClockViewModel.clock } + return if (MigrateClocksToBlueprint.isEnabled) { + Provider { keyguardClockViewModel.currentClock.value } } else { provider } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt index c40902871388..cbbb82039329 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToLockscreenTransitionViewModel.kt @@ -30,8 +30,6 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map /** * Breaks down AOD->LOCKSCREEN transition into discrete steps for corresponding views to consume. @@ -53,6 +51,8 @@ constructor( to = KeyguardState.LOCKSCREEN, ) + private var isShadeExpanded = false + /** * Begin the transition from wherever the y-translation value is currently. This helps ensure a * smooth transition if a transition in canceled. @@ -77,22 +77,21 @@ constructor( } val notificationAlpha: Flow<Float> = - combine( - shadeInteractor.shadeExpansion.map { it > 0f }, - shadeInteractor.qsExpansion.map { it > 0f }, - transitionAnimation.sharedFlow( - duration = 500.milliseconds, - onStep = { it }, - onCancel = { 1f }, - ), - ) { isShadeExpanded, isQsExpanded, alpha -> - if (isShadeExpanded || isQsExpanded) { - // One example of this happening is dragging a notification while pulsing on AOD - 1f - } else { - alpha - } - } + transitionAnimation.sharedFlow( + duration = 500.milliseconds, + onStart = { + isShadeExpanded = + shadeInteractor.shadeExpansion.value > 0f || + shadeInteractor.qsExpansion.value > 0f + }, + onStep = { + if (isShadeExpanded) { + 1f + } else { + it + } + }, + ) val shortcutsAlpha: Flow<Float> = transitionAnimation.sharedFlow( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt new file mode 100644 index 000000000000..9a23007eea4a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow + +/** + * Breaks down AOD->PRIMARY BOUNCER transition into discrete steps for corresponding views to + * consume. + */ +@ExperimentalCoroutinesApi +@SysUISingleton +class AodToPrimaryBouncerTransitionViewModel +@Inject +constructor( + animationFlow: KeyguardTransitionAnimationFlow, +) : DeviceEntryIconTransition { + private val transitionAnimation = + animationFlow.setup( + duration = FromAodTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION, + from = KeyguardState.AOD, + to = KeyguardState.PRIMARY_BOUNCER, + ) + + override val deviceEntryParentViewAlpha: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0f) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt index 4c0a9491b74a..1b91c4949018 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt @@ -55,6 +55,8 @@ constructor( lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel, dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, alternateBouncerToDozingTransitionViewModel: AlternateBouncerToDozingTransitionViewModel, + dreamingToAodTransitionViewModel: DreamingToAodTransitionViewModel, + primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, ) { val color: Flow<Int> = deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground -> @@ -96,6 +98,9 @@ constructor( lockscreenToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, alternateBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, + dreamingToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, + primaryBouncerToLockscreenTransitionViewModel + .deviceEntryBackgroundViewAlpha, ) .merge() .onStart { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index 1a018977664a..bc4fd1c88298 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.animation.FloatEvaluator import android.animation.IntEvaluator import com.android.keyguard.KeyguardViewController +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntrySourceInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor @@ -33,9 +34,11 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.util.kotlin.sample import dagger.Lazy 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.distinctUntilChanged @@ -45,6 +48,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn /** Models the UI state for the containing device entry icon & long-press handling view. */ @ExperimentalCoroutinesApi @@ -62,6 +66,7 @@ constructor( private val keyguardViewController: Lazy<KeyguardViewController>, private val deviceEntryInteractor: DeviceEntryInteractor, private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor, + @Application private val scope: CoroutineScope, ) { val isUdfpsSupported: StateFlow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported private val intEvaluator = IntEvaluator() @@ -73,7 +78,10 @@ constructor( private val qsProgress: Flow<Float> = shadeInteractor.qsExpansion.onStart { emit(0f) } private val shadeExpansion: Flow<Float> = shadeInteractor.shadeExpansion.onStart { emit(0f) } private val transitionAlpha: Flow<Float> = - transitions.map { it.deviceEntryParentViewAlpha }.merge() + transitions + .map { it.deviceEntryParentViewAlpha } + .merge() + .shareIn(scope, SharingStarted.WhileSubscribed()) private val alphaMultiplierFromShadeExpansion: Flow<Float> = combine( showingAlternateBouncer, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt new file mode 100644 index 000000000000..0fa74752ea0d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor +import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest + +/** Breaks down DREAMING->AOD transition into discrete steps for corresponding views to consume. */ +@ExperimentalCoroutinesApi +@SysUISingleton +class DreamingToAodTransitionViewModel +@Inject +constructor( + deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, + animationFlow: KeyguardTransitionAnimationFlow, +) : DeviceEntryIconTransition { + private val transitionAnimation = + animationFlow.setup( + duration = FromDreamingTransitionInteractor.TO_AOD_DURATION, + from = KeyguardState.DREAMING, + to = KeyguardState.AOD, + ) + + val deviceEntryBackgroundViewAlpha: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0f) + override val deviceEntryParentViewAlpha: Flow<Float> = + deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfpsEnrolledAndEnabled + -> + if (udfpsEnrolledAndEnabled) { + transitionAnimation.sharedFlow( + duration = FromDreamingTransitionInteractor.TO_AOD_DURATION, + onStep = { it }, + onFinish = { 1f }, + ) + } else { + emptyFlow() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt new file mode 100644 index 000000000000..ec7b931161f6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +@SysUISingleton +class DreamingToGoneTransitionViewModel +@Inject +constructor( + animationFlow: KeyguardTransitionAnimationFlow, +) { + + private val transitionAnimation = + animationFlow.setup( + duration = FromDreamingTransitionInteractor.TO_GONE_DURATION, + from = KeyguardState.DREAMING, + to = KeyguardState.GONE, + ) + + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f) + +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt index e0b1c50a84bc..a2ce408955a1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt @@ -43,9 +43,11 @@ constructor( transitionAnimation.sharedFlow( duration = 250.milliseconds, onStep = { it }, - onCancel = { 0f }, + onCancel = { 1f }, ) + val lockscreenAlpha: Flow<Float> = shortcutsAlpha + val deviceEntryBackgroundViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(1f) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index b6622e5c07b1..1c1c33ab7e7e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -26,7 +26,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.SettingsClockSize -import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode @@ -54,8 +53,6 @@ constructor( val useLargeClock: Boolean get() = clockSize.value == LARGE - var clock: ClockController? by keyguardClockInteractor::clock - val clockSize = combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) { selectedSize, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt index e35e06533f8c..8409f15dca81 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt @@ -16,10 +16,10 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -52,7 +52,7 @@ constructor( /** An observable for whether the indication area should be padded. */ val isIndicationAreaPadded: Flow<Boolean> = - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { combine(shortcutsCombinedViewModel.startButton, shortcutsCombinedViewModel.endButton) { startButtonModel, endButtonModel -> @@ -79,7 +79,7 @@ constructor( /** An observable for the x-offset by which the indication area should be translated. */ val indicationAreaTranslationX: Flow<Float> = - if (migrateClocksToBlueprint() || keyguardBottomAreaRefactor()) { + if (MigrateClocksToBlueprint.isEnabled || KeyguardBottomAreaRefactor.isEnabled) { burnIn.map { it.translationX.toFloat() } } else { bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged() @@ -87,7 +87,7 @@ constructor( /** Returns an observable for the y-offset by which the indication area should be translated. */ fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> { - return if (migrateClocksToBlueprint()) { + return if (MigrateClocksToBlueprint.isEnabled) { burnIn.map { it.translationY.toFloat() } } else { keyguardInteractor.dozeAmount diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt index b9ff25926f02..4f2c6f576904 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardPreviewClockViewModel.kt @@ -24,10 +24,8 @@ import com.android.systemui.plugins.clocks.ClockController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn /** View model for the small clock view, large clock view. */ class KeyguardPreviewClockViewModel @@ -45,15 +43,7 @@ constructor( val isSmallClockVisible: Flow<Boolean> = interactor.selectedClockSize.map { it == SettingsClockSize.SMALL } - var lastClockPair: Pair<ClockController, ClockController>? = null + val previewClock: Flow<ClockController> = interactor.previewClock - val previewClockPair: StateFlow<Pair<ClockController, ClockController>> = - interactor.previewClockPair - - val selectedClockSize: StateFlow<SettingsClockSize?> = - interactor.selectedClockSize.stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null - ) + val selectedClockSize: StateFlow<SettingsClockSize?> = interactor.selectedClockSize } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 55a402597d5b..5337ca3b9be1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -85,10 +85,13 @@ constructor( private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel, private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, + private val dreamingToGoneTransitionViewModel: DreamingToGoneTransitionViewModel, private val glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel, private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel, + private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel, + private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel, private val lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel, private val lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel, private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel, @@ -136,14 +139,20 @@ constructor( } .distinctUntilChanged() + private val lockscreenToGoneTransitionRunning: Flow<Boolean> = + keyguardTransitionInteractor + .isInTransitionWhere { from, to -> from == LOCKSCREEN && to == GONE } + .onStart { emit(false) } + private val alphaOnShadeExpansion: Flow<Float> = combineTransform( + lockscreenToGoneTransitionRunning, isOnLockscreen, shadeInteractor.qsExpansion, shadeInteractor.shadeExpansion, - ) { isOnLockscreen, qsExpansion, shadeExpansion -> + ) { lockscreenToGoneTransitionRunning, isOnLockscreen, qsExpansion, shadeExpansion -> // Fade out quickly as the shade expands - if (isOnLockscreen) { + if (isOnLockscreen && !lockscreenToGoneTransitionRunning) { val alpha = 1f - MathUtils.constrainedMap( @@ -197,17 +206,20 @@ constructor( merge( alphaOnShadeExpansion, keyguardInteractor.dismissAlpha.filterNotNull(), - alternateBouncerToGoneTransitionViewModel.lockscreenAlpha, + alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState), aodToGoneTransitionViewModel.lockscreenAlpha(viewState), aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState), aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToGoneTransitionViewModel.lockscreenAlpha(viewState), dozingToLockscreenTransitionViewModel.lockscreenAlpha, dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState), + dreamingToGoneTransitionViewModel.lockscreenAlpha, dreamingToLockscreenTransitionViewModel.lockscreenAlpha, glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, goneToAodTransitionViewModel.enterFromTopAnimationAlpha, goneToDozingTransitionViewModel.lockscreenAlpha, + goneToDreamingTransitionViewModel.lockscreenAlpha, + goneToLockscreenTransitionViewModel.lockscreenAlpha, lockscreenToAodTransitionViewModel.lockscreenAlpha(viewState), lockscreenToDozingTransitionViewModel.lockscreenAlpha, lockscreenToDreamingTransitionViewModel.lockscreenAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt index 34c9ac92a3f3..25750415e88f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow @@ -27,8 +26,6 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flatMapLatest /** * Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to @@ -39,7 +36,6 @@ import kotlinx.coroutines.flow.flatMapLatest class PrimaryBouncerToLockscreenTransitionViewModel @Inject constructor( - deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, animationFlow: KeyguardTransitionAnimationFlow, ) : DeviceEntryIconTransition { private val transitionAnimation = @@ -49,15 +45,6 @@ constructor( to = KeyguardState.LOCKSCREEN, ) - val deviceEntryBackgroundViewAlpha: Flow<Float> = - deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfps -> - if (isUdfps) { - transitionAnimation.immediatelyTransitionTo(1f) - } else { - emptyFlow() - } - } - val shortcutsAlpha: Flow<Float> = transitionAnimation.sharedFlow( duration = 250.milliseconds, @@ -67,6 +54,8 @@ constructor( val lockscreenAlpha: Flow<Float> = shortcutsAlpha + val deviceEntryBackgroundViewAlpha: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(1f) override val deviceEntryParentViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(1f) } diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt index 5f7991e62cd7..1c11178b5b35 100644 --- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt +++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt @@ -24,12 +24,13 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.lifecycleScope +import com.android.app.tracing.coroutines.createCoroutineTracingContext +import com.android.app.tracing.coroutines.launch import com.android.systemui.util.Assert import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.launch /** * Runs the given [block] every time the [View] becomes attached (or immediately after calling this @@ -66,7 +67,8 @@ fun View.repeatWhenAttached( // dispatcher to use. We don't want it to run on the Dispatchers.Default thread pool as // default behavior. Instead, we want it to run on the view's UI thread since the user will // presumably want to call view methods that require being called from said UI thread. - val lifecycleCoroutineContext = Dispatchers.Main + coroutineContext + val lifecycleCoroutineContext = + Dispatchers.Main + createCoroutineTracingContext() + coroutineContext var lifecycleOwner: ViewLifecycleOwner? = null val onAttachListener = object : View.OnAttachStateChangeListener { @@ -97,14 +99,12 @@ fun View.repeatWhenAttached( ) } - return object : DisposableHandle { - override fun dispose() { - Assert.isMainThread() + return DisposableHandle { + Assert.isMainThread() - lifecycleOwner?.onDestroy() - lifecycleOwner = null - view.removeOnAttachStateChangeListener(onAttachListener) - } + lifecycleOwner?.onDestroy() + lifecycleOwner = null + view.removeOnAttachStateChangeListener(onAttachListener) } } @@ -115,7 +115,12 @@ private fun createLifecycleOwnerAndRun( ): ViewLifecycleOwner { return ViewLifecycleOwner(view).apply { onCreate() - lifecycleScope.launch(coroutineContext) { block(view) } + lifecycleScope.launch( + "ViewLifecycleOwner(${view::class.java.simpleName})", + coroutineContext + ) { + block(view) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt new file mode 100644 index 000000000000..b6fd287a675e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.data.repository + +import android.util.Log +import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.util.MediaFlags +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +private const val TAG = "MediaDataRepository" +private const val DEBUG = true + +/** A repository that holds the state of all media controls in carousel. */ +@SysUISingleton +class MediaDataRepository +@Inject +constructor( + private val mediaFlags: MediaFlags, + dumpManager: DumpManager, +) : Dumpable { + + private val _mediaEntries: MutableStateFlow<Map<String, MediaData>> = + MutableStateFlow(LinkedHashMap()) + val mediaEntries: StateFlow<Map<String, MediaData>> = _mediaEntries.asStateFlow() + + private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> = + MutableStateFlow(SmartspaceMediaData()) + val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow() + + init { + dumpManager.registerNormalDumpable(TAG, this) + } + + /** Updates the recommendation data with a new smartspace media data. */ + fun setRecommendation(recommendation: SmartspaceMediaData) { + _smartspaceMediaData.value = recommendation + } + + /** + * Marks the recommendation data as inactive. + * + * @return true if the recommendation was actually marked as inactive, false otherwise. + */ + fun setRecommendationInactive(key: String): Boolean { + if (!mediaFlags.isPersistentSsCardEnabled()) { + Log.e(TAG, "Only persistent recommendation can be inactive!") + return false + } + if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive") + + if (smartspaceMediaData.value.targetId != key || !smartspaceMediaData.value.isValid()) { + // If this doesn't match, or we've already invalidated the data, no action needed + return false + } + + setRecommendation(smartspaceMediaData.value.copy(isActive = false)) + return true + } + + /** + * Marks the recommendation data as dismissed. + * + * @return true if the recommendation was dismissed or already inactive, false otherwise. + */ + fun dismissSmartspaceRecommendation(key: String): Boolean { + val data = smartspaceMediaData.value + if (data.targetId != key || !data.isValid()) { + // If this doesn't match, or we've already invalidated the data, no action needed + return false + } + + if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target") + if (data.isActive) { + setRecommendation( + SmartspaceMediaData( + targetId = smartspaceMediaData.value.targetId, + instanceId = smartspaceMediaData.value.instanceId + ) + ) + } + return true + } + + fun removeMediaEntry(key: String): MediaData? { + val entries = LinkedHashMap<String, MediaData>(_mediaEntries.value) + val mediaData = entries.remove(key) + _mediaEntries.value = entries + return mediaData + } + + fun addMediaEntry(key: String, data: MediaData): MediaData? { + val entries = LinkedHashMap<String, MediaData>(_mediaEntries.value) + val mediaData = entries.put(key, data) + _mediaEntries.value = entries + return mediaData + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.apply { println("mediaEntries: ${mediaEntries.value}") } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt new file mode 100644 index 000000000000..b94a4af65649 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** A repository that holds the state of filtered media data on the device. */ +@SysUISingleton +class MediaFilterRepository @Inject constructor() { + + /** Key of media control that recommendations card reactivated. */ + private val _reactivatedKey: MutableStateFlow<String?> = MutableStateFlow(null) + val reactivatedKey: StateFlow<String?> = _reactivatedKey.asStateFlow() + + private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> = + MutableStateFlow(SmartspaceMediaData()) + val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow() + + private val _selectedUserEntries: MutableStateFlow<Map<String, MediaData>> = + MutableStateFlow(LinkedHashMap()) + val selectedUserEntries: StateFlow<Map<String, MediaData>> = _selectedUserEntries.asStateFlow() + + private val _allUserEntries: MutableStateFlow<Map<String, MediaData>> = + MutableStateFlow(LinkedHashMap()) + val allUserEntries: StateFlow<Map<String, MediaData>> = _allUserEntries.asStateFlow() + + fun addMediaEntry(key: String, data: MediaData) { + val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value) + entries[key] = data + _allUserEntries.value = entries + } + + /** + * Removes the media entry corresponding to the given [key]. + * + * @return media data if an entry is actually removed, `null` otherwise. + */ + fun removeMediaEntry(key: String): MediaData? { + val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value) + val mediaData = entries.remove(key) + _allUserEntries.value = entries + return mediaData + } + + fun addSelectedUserMediaEntry(key: String, data: MediaData) { + val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) + entries[key] = data + _selectedUserEntries.value = entries + } + + /** + * Removes selected user media entry given the corresponding key. + * + * @return media data if an entry is actually removed, `null` otherwise. + */ + fun removeSelectedUserMediaEntry(key: String): MediaData? { + val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) + val mediaData = entries.remove(key) + _selectedUserEntries.value = entries + return mediaData + } + + /** + * Removes selected user media entry given a key and media data. + * + * @return true if media data is removed, false otherwise. + */ + fun removeSelectedUserMediaEntry(key: String, data: MediaData): Boolean { + val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) + val succeed = entries.remove(key, data) + if (!succeed) { + return false + } + _selectedUserEntries.value = entries + return true + } + + fun clearSelectedUserMedia() { + _selectedUserEntries.value = LinkedHashMap() + } + + /** Updates recommendation data with a new smartspace media data. */ + fun setRecommendation(smartspaceMediaData: SmartspaceMediaData) { + _smartspaceMediaData.value = smartspaceMediaData + } + + /** Updates media control key that recommendations card reactivated. */ + fun setReactivatedKey(key: String?) { + _reactivatedKey.value = key + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt new file mode 100644 index 000000000000..e0c54190283a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager +import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.util.MediaFlags +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Provider + +/** Dagger module for injecting media controls domain interfaces. */ +@Module +interface MediaDomainModule { + + @Binds + @IntoMap + @ClassKey(MediaCarouselInteractor::class) + fun bindMediaCarouselInteractor(interactor: MediaCarouselInteractor): CoreStartable + + @Binds + @IntoMap + @ClassKey(MediaDataProcessor::class) + fun bindMediaDataProcessor(interactor: MediaDataProcessor): CoreStartable + companion object { + + @Provides + @SysUISingleton + fun providesMediaDataManager( + legacyProvider: Provider<LegacyMediaDataManagerImpl>, + newProvider: Provider<MediaCarouselInteractor>, + mediaFlags: MediaFlags, + ): MediaDataManager { + return if (mediaFlags.isMediaControlsRefactorEnabled()) { + newProvider.get() + } else { + legacyProvider.get() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt index bc539efdfe69..c02478b02ec2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt @@ -61,7 +61,7 @@ internal val SMARTSPACE_MAX_AGE = * This is added at the end of the pipeline since we may still need to handle callbacks from * background users (e.g. timeouts). */ -class MediaDataFilter +class LegacyMediaDataFilterImpl @Inject constructor( private val context: Context, @@ -74,9 +74,9 @@ constructor( private val mediaFlags: MediaFlags, ) : MediaDataManager.Listener { private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() - internal val listeners: Set<MediaDataManager.Listener> + val listeners: Set<MediaDataManager.Listener> get() = _listeners.toSet() - internal lateinit var mediaDataManager: MediaDataManager + lateinit var mediaDataManager: MediaDataManager private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager @@ -279,7 +279,7 @@ constructor( val mediaKeys = userEntries.keys.toSet() mediaKeys.forEach { // Force updates to listeners, needed for re-activated card - mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true) + mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true) } if (smartspaceMediaData.isActive) { val dismissIntent = smartspaceMediaData.dismissIntent diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt new file mode 100644 index 000000000000..3a83115642bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt @@ -0,0 +1,1693 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline + +import android.annotation.SuppressLint +import android.app.ActivityOptions +import android.app.BroadcastOptions +import android.app.Notification +import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME +import android.app.PendingIntent +import android.app.StatusBarManager +import android.app.UriGrantsManager +import android.app.smartspace.SmartspaceAction +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.app.smartspace.SmartspaceTarget +import android.content.BroadcastReceiver +import android.content.ContentProvider +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.graphics.drawable.Animatable +import android.graphics.drawable.Icon +import android.media.MediaDescription +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.net.Uri +import android.os.Parcelable +import android.os.Process +import android.os.UserHandle +import android.provider.Settings +import android.service.notification.StatusBarNotification +import android.support.v4.media.MediaMetadataCompat +import android.text.TextUtils +import android.util.Log +import android.util.Pair as APair +import androidx.media.utils.MediaConstants +import com.android.app.tracing.traceSection +import com.android.internal.annotations.Keep +import com.android.internal.logging.InstanceId +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Dumpable +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification +import com.android.systemui.media.controls.domain.resume.MediaResumeListener +import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser +import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE +import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC +import com.android.systemui.media.controls.shared.model.MediaAction +import com.android.systemui.media.controls.shared.model.MediaButton +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDeviceData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.ui.view.MediaViewHolder +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaDataUtils +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.res.R +import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState +import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState +import com.android.systemui.statusbar.notification.row.HybridGroupManager +import com.android.systemui.tuner.TunerService +import com.android.systemui.util.Assert +import com.android.systemui.util.Utils +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.concurrency.ThreadFactory +import com.android.systemui.util.time.SystemClock +import java.io.IOException +import java.io.PrintWriter +import java.util.concurrent.Executor +import javax.inject.Inject + +// URI fields to try loading album art from +private val ART_URIS = + arrayOf( + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI + ) + +private const val TAG = "MediaDataManager" +private const val DEBUG = true +private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent" + +private val LOADING = + MediaData( + userId = -1, + initialized = false, + app = null, + appIcon = null, + artist = null, + song = null, + artwork = null, + actions = emptyList(), + actionsToShowInCompact = emptyList(), + packageName = "INVALID", + token = null, + clickIntent = null, + device = null, + active = true, + resumeAction = null, + instanceId = InstanceId.fakeInstanceId(-1), + appUid = Process.INVALID_UID + ) + +internal val EMPTY_SMARTSPACE_MEDIA_DATA = + SmartspaceMediaData( + targetId = "INVALID", + isActive = false, + packageName = "INVALID", + cardAction = null, + recommendations = emptyList(), + dismissIntent = null, + headphoneConnectionTimeMillis = 0, + instanceId = InstanceId.fakeInstanceId(-1), + expiryTimeMs = 0, + ) + +const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank." + +/** + * Allow recommendations from smartspace to show in media controls. Requires + * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0 + */ +private fun allowMediaRecommendations(context: Context): Boolean { + val flag = + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 + ) + return Utils.useQsMediaPlayer(context) && flag > 0 +} + +/** A class that facilitates management and loading of Media Data, ready for binding. */ +@SysUISingleton +class LegacyMediaDataManagerImpl( + private val context: Context, + @Background private val backgroundExecutor: Executor, + @Main private val uiExecutor: Executor, + @Main private val foregroundExecutor: DelayableExecutor, + private val mediaControllerFactory: MediaControllerFactory, + private val broadcastDispatcher: BroadcastDispatcher, + dumpManager: DumpManager, + mediaTimeoutListener: MediaTimeoutListener, + mediaResumeListener: MediaResumeListener, + mediaSessionBasedFilter: MediaSessionBasedFilter, + private val mediaDeviceManager: MediaDeviceManager, + mediaDataCombineLatest: MediaDataCombineLatest, + private val mediaDataFilter: LegacyMediaDataFilterImpl, + private val activityStarter: ActivityStarter, + private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider, + private var useMediaResumption: Boolean, + private val useQsMediaPlayer: Boolean, + private val systemClock: SystemClock, + private val tunerService: TunerService, + private val mediaFlags: MediaFlags, + private val logger: MediaUiEventLogger, + private val smartspaceManager: SmartspaceManager?, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, +) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener, MediaDataManager { + + companion object { + // UI surface label for subscribing Smartspace updates. + @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" + + // Smartspace package name's extra key. + @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" + + // Maximum number of actions allowed in compact view + @JvmField val MAX_COMPACT_ACTIONS = 3 + + // Maximum number of actions allowed in expanded view + @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size + } + + private val themeText = + com.android.settingslib.Utils.getColorAttr( + context, + com.android.internal.R.attr.textColorPrimary + ) + .defaultColor + + // Internal listeners are part of the internal pipeline. External listeners (those registered + // with [MediaDeviceManager.addListener]) receive events after they have propagated through + // the internal pipeline. + // Another way to think of the distinction between internal and external listeners is the + // following. Internal listeners are listeners that MediaDataManager depends on, and external + // listeners are listeners that depend on MediaDataManager. + // TODO(b/159539991#comment5): Move internal listeners to separate package. + private val internalListeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() + private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + // There should ONLY be at most one Smartspace media recommendation. + var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA + @Keep private var smartspaceSession: SmartspaceSession? = null + private var allowMediaRecommendations = allowMediaRecommendations(context) + + private val artworkWidth = + context.resources.getDimensionPixelSize( + com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize + ) + private val artworkHeight = + context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) + + @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE + private val statusBarManager = + context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager + + /** Check whether this notification is an RCN */ + private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean { + return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) + } + + @Inject + constructor( + context: Context, + threadFactory: ThreadFactory, + @Main uiExecutor: Executor, + @Main foregroundExecutor: DelayableExecutor, + mediaControllerFactory: MediaControllerFactory, + dumpManager: DumpManager, + broadcastDispatcher: BroadcastDispatcher, + mediaTimeoutListener: MediaTimeoutListener, + mediaResumeListener: MediaResumeListener, + mediaSessionBasedFilter: MediaSessionBasedFilter, + mediaDeviceManager: MediaDeviceManager, + mediaDataCombineLatest: MediaDataCombineLatest, + mediaDataFilter: LegacyMediaDataFilterImpl, + activityStarter: ActivityStarter, + smartspaceMediaDataProvider: SmartspaceMediaDataProvider, + clock: SystemClock, + tunerService: TunerService, + mediaFlags: MediaFlags, + logger: MediaUiEventLogger, + smartspaceManager: SmartspaceManager?, + keyguardUpdateMonitor: KeyguardUpdateMonitor, + ) : this( + context, + // Loading bitmap for UMO background can take longer time, so it cannot run on the default + // background thread. Use a custom thread for media. + threadFactory.buildExecutorOnNewThread(TAG), + uiExecutor, + foregroundExecutor, + mediaControllerFactory, + broadcastDispatcher, + dumpManager, + mediaTimeoutListener, + mediaResumeListener, + mediaSessionBasedFilter, + mediaDeviceManager, + mediaDataCombineLatest, + mediaDataFilter, + activityStarter, + smartspaceMediaDataProvider, + Utils.useMediaResumption(context), + Utils.useQsMediaPlayer(context), + clock, + tunerService, + mediaFlags, + logger, + smartspaceManager, + keyguardUpdateMonitor, + ) + + private val appChangeReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + Intent.ACTION_PACKAGES_SUSPENDED -> { + val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) + packages?.forEach { removeAllForPackage(it) } + } + Intent.ACTION_PACKAGE_REMOVED, + Intent.ACTION_PACKAGE_RESTARTED -> { + intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) } + } + } + } + } + + init { + dumpManager.registerDumpable(TAG, this) + + // Initialize the internal processing pipeline. The listeners at the front of the pipeline + // are set as internal listeners so that they receive events. From there, events are + // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, + // so it is responsible for dispatching events to external listeners. To achieve this, + // external listeners that are registered with [MediaDataManager.addListener] are actually + // registered as listeners to mediaDataFilter. + addInternalListener(mediaTimeoutListener) + addInternalListener(mediaResumeListener) + addInternalListener(mediaSessionBasedFilter) + mediaSessionBasedFilter.addListener(mediaDeviceManager) + mediaSessionBasedFilter.addListener(mediaDataCombineLatest) + mediaDeviceManager.addListener(mediaDataCombineLatest) + mediaDataCombineLatest.addListener(mediaDataFilter) + + // Set up links back into the pipeline for listeners that need to send events upstream. + mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> + setInactive(key, timedOut) + } + mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> + updateState(key, state) + } + mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) } + mediaResumeListener.setManager(this) + mediaDataFilter.mediaDataManager = this + + val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) + broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) + + val uninstallFilter = + IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_REMOVED) + addAction(Intent.ACTION_PACKAGE_RESTARTED) + addDataScheme("package") + } + // BroadcastDispatcher does not allow filters with data schemes + context.registerReceiver(appChangeReceiver, uninstallFilter) + + // Register for Smartspace data updates. + smartspaceMediaDataProvider.registerListener(this) + smartspaceSession = + smartspaceManager?.createSmartspaceSession( + SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() + ) + smartspaceSession?.let { + it.addOnTargetsAvailableListener( + // Use a main uiExecutor thread listening to Smartspace updates instead of using + // the existing background executor. + // SmartspaceSession has scheduled routine updates which can be unpredictable on + // test simulators, using the backgroundExecutor makes it's hard to test the threads + // numbers. + uiExecutor, + SmartspaceSession.OnTargetsAvailableListener { targets -> + smartspaceMediaDataProvider.onTargetsAvailable(targets) + } + ) + } + smartspaceSession?.let { it.requestSmartspaceUpdate() } + tunerService.addTunable( + object : TunerService.Tunable { + override fun onTuningChanged(key: String?, newValue: String?) { + allowMediaRecommendations = allowMediaRecommendations(context) + if (!allowMediaRecommendations) { + dismissSmartspaceRecommendation( + key = smartspaceMediaData.targetId, + delay = 0L + ) + } + } + }, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION + ) + } + + override fun destroy() { + smartspaceMediaDataProvider.unregisterListener(this) + smartspaceSession?.close() + smartspaceSession = null + context.unregisterReceiver(appChangeReceiver) + } + + override fun onNotificationAdded(key: String, sbn: StatusBarNotification) { + if (useQsMediaPlayer && isMediaNotification(sbn)) { + var isNewlyActiveEntry = false + Assert.isMainThread() + val oldKey = findExistingEntry(key, sbn.packageName) + if (oldKey == null) { + val instanceId = logger.getNewInstanceId() + val temp = + LOADING.copy( + packageName = sbn.packageName, + instanceId = instanceId, + createdTimestampMillis = systemClock.currentTimeMillis(), + ) + mediaEntries.put(key, temp) + isNewlyActiveEntry = true + } else if (oldKey != key) { + // Resume -> active conversion; move to new key + val oldData = mediaEntries.remove(oldKey)!! + isNewlyActiveEntry = true + mediaEntries.put(key, oldData) + } + loadMediaData(key, sbn, oldKey, isNewlyActiveEntry) + } else { + onNotificationRemoved(key) + } + } + + private fun removeAllForPackage(packageName: String) { + Assert.isMainThread() + val toRemove = mediaEntries.filter { it.value.packageName == packageName } + toRemove.forEach { removeEntry(it.key) } + } + + override fun setResumeAction(key: String, action: Runnable?) { + mediaEntries.get(key)?.let { + it.resumeAction = action + it.hasCheckedForResume = true + } + } + + override fun addResumptionControls( + userId: Int, + desc: MediaDescription, + action: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + // Resume controls don't have a notification key, so store by package name instead + if (!mediaEntries.containsKey(packageName)) { + val instanceId = logger.getNewInstanceId() + val appUid = + try { + context.packageManager.getApplicationInfo(packageName, 0)?.uid!! + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app UID for $packageName", e) + Process.INVALID_UID + } + + val resumeData = + LOADING.copy( + packageName = packageName, + resumeAction = action, + hasCheckedForResume = true, + instanceId = instanceId, + appUid = appUid, + createdTimestampMillis = systemClock.currentTimeMillis(), + ) + mediaEntries.put(packageName, resumeData) + logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) + logger.logResumeMediaAdded(appUid, packageName, instanceId) + } + backgroundExecutor.execute { + loadMediaDataInBgForResumption( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) + } + } + + /** + * Check if there is an existing entry that matches the key or package name. Returns the key + * that matches, or null if not found. + */ + private fun findExistingEntry(key: String, packageName: String): String? { + if (mediaEntries.containsKey(key)) { + return key + } + // Check if we already had a resume player + if (mediaEntries.containsKey(packageName)) { + return packageName + } + return null + } + + private fun loadMediaData( + key: String, + sbn: StatusBarNotification, + oldKey: String?, + isNewlyActiveEntry: Boolean = false, + ) { + backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } + } + + /** Add a listener for changes in this class */ + override fun addListener(listener: MediaDataManager.Listener) { + // mediaDataFilter is the current end of the internal pipeline. Register external + // listeners as listeners to it. + mediaDataFilter.addListener(listener) + } + + /** Remove a listener for changes in this class */ + override fun removeListener(listener: MediaDataManager.Listener) { + // Since mediaDataFilter is the current end of the internal pipelie, external listeners + // have been registered to it. So, they need to be removed from it too. + mediaDataFilter.removeListener(listener) + } + + /** Add a listener for internal events. */ + private fun addInternalListener(listener: MediaDataManager.Listener) = + internalListeners.add(listener) + + /** + * Notify internal listeners of media loaded event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + */ + private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { + internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) } + } + + /** + * Notify internal listeners of Smartspace media loaded event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + */ + private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { + internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) } + } + + /** + * Notify internal listeners of media removed event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + */ + private fun notifyMediaDataRemoved(key: String) { + internalListeners.forEach { it.onMediaDataRemoved(key) } + } + + /** + * Notify internal listeners of Smartspace media removed event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait until + * the next refresh-round before UI becomes visible. Should only be true if the update is + * initiated by user's interaction. + */ + private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } + } + + /** + * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This + * will make the player not active anymore, hiding it from QQS and Keyguard. + * + * @see MediaData.active + */ + override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) { + mediaEntries[key]?.let { + if (timedOut && !forceUpdate) { + // Only log this event when media expires on its own + logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId) + } + if (it.active == !timedOut && !forceUpdate) { + if (it.resumption) { + if (DEBUG) Log.d(TAG, "timing out resume player $key") + dismissMediaData(key, 0L /* delay */) + } + return + } + // Update last active if media was still active. + if (it.active) { + it.lastActive = systemClock.elapsedRealtime() + } + it.active = !timedOut + if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") + onMediaDataLoaded(key, key, it) + } + + if (key == smartspaceMediaData.targetId) { + if (DEBUG) Log.d(TAG, "smartspace card expired") + dismissSmartspaceRecommendation(key, delay = 0L) + } + } + + /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ + private fun updateState(key: String, state: PlaybackState) { + mediaEntries.get(key)?.let { + val token = it.token + if (token == null) { + if (DEBUG) Log.d(TAG, "State updated, but token was null") + return + } + val actions = + createActionsFromState( + it.packageName, + mediaControllerFactory.create(it.token), + UserHandle(it.userId) + ) + + // Control buttons + // If flag is enabled and controller has a PlaybackState, + // create actions from session info + // otherwise, no need to update semantic actions. + val data = + if (actions != null) { + it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) + } else { + it.copy(isPlaying = isPlayingState(state.state)) + } + if (DEBUG) Log.d(TAG, "State updated outside of notification") + onMediaDataLoaded(key, key, data) + } + } + + private fun removeEntry(key: String, logEvent: Boolean = true) { + mediaEntries.remove(key)?.let { + if (logEvent) { + logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) + } + } + notifyMediaDataRemoved(key) + } + + /** Dismiss a media entry. Returns false if the key was not found. */ + override fun dismissMediaData(key: String, delay: Long): Boolean { + val existed = mediaEntries[key] != null + backgroundExecutor.execute { + mediaEntries[key]?.let { mediaData -> + if (mediaData.isLocalSession()) { + mediaData.token?.let { + val mediaController = mediaControllerFactory.create(it) + mediaController.transportControls.stop() + } + } + } + } + foregroundExecutor.executeDelayed({ removeEntry(key) }, delay) + return existed + } + + /** + * Called whenever the recommendation has been expired or removed by the user. This will remove + * the recommendation card entirely from the carousel. + */ + override fun dismissSmartspaceRecommendation(key: String, delay: Long) { + if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { + // If this doesn't match, or we've already invalidated the data, no action needed + return + } + + if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target") + if (smartspaceMediaData.isActive) { + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) + } + foregroundExecutor.executeDelayed( + { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) }, + delay + ) + } + + /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ + override fun setRecommendationInactive(key: String) { + if (!mediaFlags.isPersistentSsCardEnabled()) { + Log.e(TAG, "Only persistent recommendation can be inactive!") + return + } + if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive") + + if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { + // If this doesn't match, or we've already invalidated the data, no action needed + return + } + + smartspaceMediaData = smartspaceMediaData.copy(isActive = false) + notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) + } + + private fun loadMediaDataInBgForResumption( + userId: Int, + desc: MediaDescription, + resumeAction: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + if (desc.title.isNullOrBlank()) { + Log.e(TAG, "Description incomplete") + // Delete the placeholder entry + mediaEntries.remove(packageName) + return + } + + if (DEBUG) { + Log.d(TAG, "adding track for $userId from browser: $desc") + } + + val currentEntry = mediaEntries.get(packageName) + val appUid = currentEntry?.appUid ?: Process.INVALID_UID + + // Album art + var artworkBitmap = desc.iconBitmap + if (artworkBitmap == null && desc.iconUri != null) { + artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName) + } + val artworkIcon = + if (artworkBitmap != null) { + Icon.createWithBitmap(artworkBitmap) + } else { + null + } + + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val isExplicit = + desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + + val progress = + if (mediaFlags.isResumeProgressEnabled()) { + MediaDataUtils.getDescriptionProgress(desc.extras) + } else null + + val mediaAction = getResumeMediaAction(resumeAction) + val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + foregroundExecutor.execute { + onMediaDataLoaded( + packageName, + null, + MediaData( + userId, + true, + appName, + null, + desc.subtitle, + desc.title, + artworkIcon, + listOf(mediaAction), + listOf(0), + MediaButton(playOrPause = mediaAction), + packageName, + token, + appIntent, + device = null, + active = false, + resumeAction = resumeAction, + resumption = true, + notificationKey = packageName, + hasCheckedForResume = true, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = appUid, + isExplicit = isExplicit, + resumeProgress = progress, + ) + ) + } + } + + fun loadMediaDataInBg( + key: String, + sbn: StatusBarNotification, + oldKey: String?, + isNewlyActiveEntry: Boolean = false, + ) { + val token = + sbn.notification.extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION, + MediaSession.Token::class.java + ) + if (token == null) { + return + } + val mediaController = mediaControllerFactory.create(token) + val metadata = mediaController.metadata + val notif: Notification = sbn.notification + + val appInfo = + notif.extras.getParcelable( + Notification.EXTRA_BUILDER_APPLICATION_INFO, + ApplicationInfo::class.java + ) + ?: getAppInfoFromPackage(sbn.packageName) + + // App name + val appName = getAppName(sbn, appInfo) + + // Song name + var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (song.isNullOrBlank()) { + song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) + } + if (song.isNullOrBlank()) { + song = HybridGroupManager.resolveTitle(notif) + } + if (song.isNullOrBlank()) { + // For apps that don't include a title, log and add a placeholder + song = context.getString(R.string.controls_media_empty_title, appName) + try { + statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier) + } catch (e: RuntimeException) { + Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}") + } + } + + // Album art + var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } + if (artworkBitmap == null) { + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) + } + if (artworkBitmap == null) { + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + } + val artWorkIcon = + if (artworkBitmap == null) { + notif.getLargeIcon() + } else { + Icon.createWithBitmap(artworkBitmap) + } + + // App Icon + val smallIcon = sbn.notification.smallIcon + + // Explicit Indicator + var isExplicit = false + val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) + isExplicit = + mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + + // Artist name + var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) + if (artist.isNullOrBlank()) { + artist = HybridGroupManager.resolveText(notif) + } + + // Device name (used for remote cast notifications) + var device: MediaDeviceData? = null + if (isRemoteCastNotification(sbn)) { + val extras = sbn.notification.extras + val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) + val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) + val deviceIntent = + extras.getParcelable( + Notification.EXTRA_MEDIA_REMOTE_INTENT, + PendingIntent::class.java + ) + Log.d(TAG, "$key is RCN for $deviceName") + + if (deviceName != null && deviceIcon > -1) { + // Name and icon must be present, but intent may be null + val enabled = deviceIntent != null && deviceIntent.isActivity + val deviceDrawable = + Icon.createWithResource(sbn.packageName, deviceIcon) + .loadDrawable(sbn.getPackageContext(context)) + device = + MediaDeviceData( + enabled, + deviceDrawable, + deviceName, + deviceIntent, + showBroadcastButton = false + ) + } + } + + // Control buttons + // If flag is enabled and controller has a PlaybackState, create actions from session info + // Otherwise, use the notification actions + var actionIcons: List<MediaAction> = emptyList() + var actionsToShowCollapsed: List<Int> = emptyList() + val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) + if (semanticActions == null) { + val actions = createActionsFromNotification(sbn) + actionIcons = actions.first + actionsToShowCollapsed = actions.second + } + + val playbackLocation = + if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE + else if ( + mediaController.playbackInfo?.playbackType == + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL + ) + MediaData.PLAYBACK_LOCAL + else MediaData.PLAYBACK_CAST_LOCAL + val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null + + val currentEntry = mediaEntries.get(key) + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val appUid = appInfo?.uid ?: Process.INVALID_UID + + if (isNewlyActiveEntry) { + logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) + logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) + } else if (playbackLocation != currentEntry?.playbackLocation) { + logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation) + } + + val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + foregroundExecutor.execute { + val resumeAction: Runnable? = mediaEntries[key]?.resumeAction + val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true + val active = mediaEntries[key]?.active ?: true + onMediaDataLoaded( + key, + oldKey, + MediaData( + sbn.normalizedUserId, + true, + appName, + smallIcon, + artist, + song, + artWorkIcon, + actionIcons, + actionsToShowCollapsed, + semanticActions, + sbn.packageName, + token, + notif.contentIntent, + device, + active, + resumeAction = resumeAction, + playbackLocation = playbackLocation, + notificationKey = key, + hasCheckedForResume = hasCheckedForResume, + isPlaying = isPlaying, + isClearable = !sbn.isOngoing, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = appUid, + isExplicit = isExplicit, + ) + ) + } + } + + private fun logSingleVsMultipleMediaAdded( + appUid: Int, + packageName: String, + instanceId: InstanceId + ) { + if (mediaEntries.size == 1) { + logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) + } else if (mediaEntries.size == 2) { + // Since this method is only called when there is a new media session added. + // logging needed once there is more than one media session in carousel. + logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId) + } + } + + private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { + try { + return context.packageManager.getApplicationInfo(packageName, 0) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app info for $packageName", e) + } + return null + } + + private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { + val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) + if (name != null) { + return name + } + + return if (appInfo != null) { + context.packageManager.getApplicationLabel(appInfo).toString() + } else { + sbn.packageName + } + } + + /** Generate action buttons based on notification actions */ + private fun createActionsFromNotification( + sbn: StatusBarNotification + ): Pair<List<MediaAction>, List<Int>> { + val notif = sbn.notification + val actionIcons: MutableList<MediaAction> = ArrayList() + val actions = notif.actions + var actionsToShowCollapsed = + notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() + ?: mutableListOf() + if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { + Log.e( + TAG, + "Too many compact actions for ${sbn.key}," + + "limiting to first $MAX_COMPACT_ACTIONS" + ) + actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) + } + + if (actions != null) { + for ((index, action) in actions.withIndex()) { + if (index == MAX_NOTIFICATION_ACTIONS) { + Log.w( + TAG, + "Too many notification actions for ${sbn.key}," + + " limiting to first $MAX_NOTIFICATION_ACTIONS" + ) + break + } + if (action.getIcon() == null) { + if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}") + actionsToShowCollapsed.remove(index) + continue + } + val runnable = + if (action.actionIntent != null) { + Runnable { + if (action.actionIntent.isActivity) { + activityStarter.startPendingIntentDismissingKeyguard( + action.actionIntent + ) + } else if (action.isAuthenticationRequired()) { + activityStarter.dismissKeyguardThenExecute( + { + var result = sendPendingIntent(action.actionIntent) + result + }, + {}, + true + ) + } else { + sendPendingIntent(action.actionIntent) + } + } + } else { + null + } + val mediaActionIcon = + if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { + Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) + } else { + action.getIcon() + } + .setTint(themeText) + .loadDrawable(context) + val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) + actionIcons.add(mediaAction) + } + } + return Pair(actionIcons, actionsToShowCollapsed) + } + + /** + * Generates action button info for this media session based on the PlaybackState + * + * @param packageName Package name for the media app + * @param controller MediaController for the current session + * @return a Pair consisting of a list of media actions, and a list of ints representing which + * + * ``` + * of those actions should be shown in the compact player + * ``` + */ + private fun createActionsFromState( + packageName: String, + controller: MediaController, + user: UserHandle + ): MediaButton? { + val state = controller.playbackState + if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { + return null + } + + // First, check for standard actions + val playOrPause = + if (isConnectingState(state.state)) { + // Spinner needs to be animating to render anything. Start it here. + val drawable = + context.getDrawable(com.android.internal.R.drawable.progress_small_material) + (drawable as Animatable).start() + MediaAction( + drawable, + null, // no action to perform when clicked + context.getString(R.string.controls_media_button_connecting), + context.getDrawable(R.drawable.ic_media_connecting_container), + // Specify a rebind id to prevent the spinner from restarting on later binds. + com.android.internal.R.drawable.progress_small_material + ) + } else if (isPlayingState(state.state)) { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) + } else { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) + } + val prevButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) + val nextButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) + + // Then, create a way to build any custom actions that will be needed + val customActions = + state.customActions + .asSequence() + .filterNotNull() + .map { getCustomAction(state, packageName, controller, it) } + .iterator() + fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null + + // Finally, assign the remaining button slots: play/pause A B C D + // A = previous, else custom action (if not reserved) + // B = next, else custom action (if not reserved) + // C and D are always custom actions + val reservePrev = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV + ) == true + val reserveNext = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT + ) == true + + val prevOrCustom = + if (prevButton != null) { + prevButton + } else if (!reservePrev) { + nextCustomAction() + } else { + null + } + + val nextOrCustom = + if (nextButton != null) { + nextButton + } else if (!reserveNext) { + nextCustomAction() + } else { + null + } + + return MediaButton( + playOrPause, + nextOrCustom, + prevOrCustom, + nextCustomAction(), + nextCustomAction(), + reserveNext, + reservePrev + ) + } + + /** + * Create a [MediaAction] for a given action and media session + * + * @param controller MediaController for the session + * @param stateActions The actions included with the session's [PlaybackState] + * @param action A [PlaybackState.Actions] value representing what action to generate. One of: + * ``` + * [PlaybackState.ACTION_PLAY] + * [PlaybackState.ACTION_PAUSE] + * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] + * [PlaybackState.ACTION_SKIP_TO_NEXT] + * @return + * ``` + * + * A [MediaAction] with correct values set, or null if the state doesn't support it + */ + private fun getStandardAction( + controller: MediaController, + stateActions: Long, + @PlaybackState.Actions action: Long + ): MediaAction? { + if (!includesAction(stateActions, action)) { + return null + } + + return when (action) { + PlaybackState.ACTION_PLAY -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_play), + { controller.transportControls.play() }, + context.getString(R.string.controls_media_button_play), + context.getDrawable(R.drawable.ic_media_play_container) + ) + } + PlaybackState.ACTION_PAUSE -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_pause), + { controller.transportControls.pause() }, + context.getString(R.string.controls_media_button_pause), + context.getDrawable(R.drawable.ic_media_pause_container) + ) + } + PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_prev), + { controller.transportControls.skipToPrevious() }, + context.getString(R.string.controls_media_button_prev), + null + ) + } + PlaybackState.ACTION_SKIP_TO_NEXT -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_next), + { controller.transportControls.skipToNext() }, + context.getString(R.string.controls_media_button_next), + null + ) + } + else -> null + } + } + + /** Check whether the actions from a [PlaybackState] include a specific action */ + private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { + if ( + (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && + (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) + ) { + return true + } + return (stateActions and action != 0L) + } + + /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ + private fun getCustomAction( + state: PlaybackState, + packageName: String, + controller: MediaController, + customAction: PlaybackState.CustomAction + ): MediaAction { + return MediaAction( + Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), + { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, + customAction.name, + null + ) + } + + /** Load a bitmap from the various Art metadata URIs */ + private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { + for (uri in ART_URIS) { + val uriString = metadata.getString(uri) + if (!TextUtils.isEmpty(uriString)) { + val albumArt = loadBitmapFromUri(Uri.parse(uriString)) + if (albumArt != null) { + if (DEBUG) Log.d(TAG, "loaded art from $uri") + return albumArt + } + } + } + return null + } + + private fun sendPendingIntent(intent: PendingIntent): Boolean { + return try { + val options = BroadcastOptions.makeBasic() + options.setInteractive(true) + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + intent.send(options.toBundle()) + true + } catch (e: PendingIntent.CanceledException) { + Log.d(TAG, "Intent canceled", e) + false + } + } + + /** Returns a bitmap if the user can access the given URI, else null */ + private fun loadBitmapFromUriForUser( + uri: Uri, + userId: Int, + appUid: Int, + packageName: String, + ): Bitmap? { + try { + val ugm = UriGrantsManager.getService() + ugm.checkGrantUriPermission_ignoreNonSystem( + appUid, + packageName, + ContentProvider.getUriWithoutUserId(uri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(uri, userId) + ) + return loadBitmapFromUri(uri) + } catch (e: SecurityException) { + Log.e(TAG, "Failed to get URI permission: $e") + } + return null + } + + /** + * Load a bitmap from a URI + * + * @param uri the uri to load + * @return bitmap, or null if couldn't be loaded + */ + private fun loadBitmapFromUri(uri: Uri): Bitmap? { + // ImageDecoder requires a scheme of the following types + if (uri.scheme == null) { + return null + } + + if ( + !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && + !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) && + !uri.scheme.equals(ContentResolver.SCHEME_FILE) + ) { + return null + } + + val source = ImageDecoder.createSource(context.contentResolver, uri) + return try { + ImageDecoder.decodeBitmap(source) { decoder, info, _ -> + val width = info.size.width + val height = info.size.height + val scale = + MediaDataUtils.getScaleFactor( + APair(width, height), + APair(artworkWidth, artworkHeight) + ) + + // Downscale if needed + if (scale != 0f && scale < 1) { + decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt()) + } + decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE + } + } catch (e: IOException) { + Log.e(TAG, "Unable to load bitmap", e) + null + } catch (e: RuntimeException) { + Log.e(TAG, "Unable to load bitmap", e) + null + } + } + + private fun getResumeMediaAction(action: Runnable): MediaAction { + return MediaAction( + Icon.createWithResource(context, R.drawable.ic_media_play) + .setTint(themeText) + .loadDrawable(context), + action, + context.getString(R.string.controls_media_resume), + context.getDrawable(R.drawable.ic_media_play_container) + ) + } + + fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = + traceSection("MediaDataManager#onMediaDataLoaded") { + Assert.isMainThread() + if (mediaEntries.containsKey(key)) { + // Otherwise this was removed already + mediaEntries.put(key, data) + notifyMediaDataLoaded(key, oldKey, data) + } + } + + override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { + if (!allowMediaRecommendations) { + if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.") + return + } + + val mediaTargets = targets.filterIsInstance<SmartspaceTarget>() + when (mediaTargets.size) { + 0 -> { + if (!smartspaceMediaData.isActive) { + return + } + if (DEBUG) { + Log.d(TAG, "Set Smartspace media to be inactive for the data update") + } + if (mediaFlags.isPersistentSsCardEnabled()) { + // Smartspace uses this signal to hide the card (e.g. when it expires or user + // disconnects headphones), so treat as setting inactive when flag is on + smartspaceMediaData = smartspaceMediaData.copy(isActive = false) + notifySmartspaceMediaDataLoaded( + smartspaceMediaData.targetId, + smartspaceMediaData, + ) + } else { + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId, + ) + notifySmartspaceMediaDataRemoved( + smartspaceMediaData.targetId, + immediately = false, + ) + } + } + 1 -> { + val newMediaTarget = mediaTargets.get(0) + if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) { + // The same Smartspace updates can be received. Skip the duplicate updates. + return + } + if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") + smartspaceMediaData = toSmartspaceMediaData(newMediaTarget) + notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) + } + else -> { + // There should NOT be more than 1 Smartspace media update. When it happens, it + // indicates a bad state or an error. Reset the status accordingly. + Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") + notifySmartspaceMediaDataRemoved( + smartspaceMediaData.targetId, + immediately = false, + ) + smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA + } + } + } + + override fun onNotificationRemoved(key: String) { + Assert.isMainThread() + val removed = mediaEntries.remove(key) ?: return + if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } else if (isAbleToResume(removed)) { + convertToResumePlayer(key, removed) + } else if (mediaFlags.isRetainingPlayersEnabled()) { + handlePossibleRemoval(key, removed, notificationRemoved = true) + } else { + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } + } + + private fun onSessionDestroyed(key: String) { + if (DEBUG) Log.d(TAG, "session destroyed for $key") + val entry = mediaEntries.remove(key) ?: return + // Clear token since the session is no longer valid + val updated = entry.copy(token = null) + handlePossibleRemoval(key, updated) + } + + private fun isAbleToResume(data: MediaData): Boolean { + val isEligibleForResume = + data.isLocalSession() || + (mediaFlags.isRemoteResumeAllowed() && + data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) + return useMediaResumption && data.resumeAction != null && isEligibleForResume + } + + /** + * Convert to resume state if the player is no longer valid and active, then notify listeners + * that the data was updated. Does not convert to resume state if the player is still valid, or + * if it was removed before becoming inactive. (Assumes that [removed] was removed from + * [mediaEntries] before this function was called) + */ + private fun handlePossibleRemoval( + key: String, + removed: MediaData, + notificationRemoved: Boolean = false + ) { + val hasSession = removed.token != null + if (hasSession && removed.semanticActions != null) { + // The app was using session actions, and the session is still valid: keep player + if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key") + mediaEntries.put(key, removed) + notifyMediaDataLoaded(key, key, removed) + } else if (!notificationRemoved && removed.semanticActions == null) { + // The app was using notification actions, and notif wasn't removed yet: keep player + if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key") + mediaEntries.put(key, removed) + notifyMediaDataLoaded(key, key, removed) + } else if (removed.active && !isAbleToResume(removed)) { + // This player was still active - it didn't last long enough to time out, + // and its app doesn't normally support resume: remove + if (DEBUG) Log.d(TAG, "Removing still-active player $key") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) { + // Convert to resume + if (DEBUG) { + Log.d( + TAG, + "Notification ($notificationRemoved) and/or session " + + "($hasSession) gone for inactive player $key" + ) + } + convertToResumePlayer(key, removed) + } else { + // Retaining players flag is off and app doesn't support resume: remove player. + if (DEBUG) Log.d(TAG, "Removing player $key") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } + } + + /** Set the given [MediaData] as a resume state player and notify listeners */ + private fun convertToResumePlayer(key: String, data: MediaData) { + if (DEBUG) Log.d(TAG, "Converting $key to resume") + // Resumption controls must have a title. + if (data.song.isNullOrBlank()) { + Log.e(TAG, "Description incomplete") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) + return + } + // Move to resume key (aka package name) if that key doesn't already exist. + val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) } + val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList() + val launcherIntent = + context.packageManager.getLaunchIntentForPackage(data.packageName)?.let { + PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE) + } + val lastActive = + if (data.active) { + systemClock.elapsedRealtime() + } else { + data.lastActive + } + val updated = + data.copy( + token = null, + actions = actions, + semanticActions = MediaButton(playOrPause = resumeAction), + actionsToShowInCompact = listOf(0), + active = false, + resumption = true, + isPlaying = false, + isClearable = true, + clickIntent = launcherIntent, + lastActive = lastActive, + ) + val pkg = data.packageName + val migrate = mediaEntries.put(pkg, updated) == null + // Notify listeners of "new" controls when migrating or removed and update when not + Log.d(TAG, "migrating? $migrate from $key -> $pkg") + if (migrate) { + notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated) + } else { + // Since packageName is used for the key of the resumption controls, it is + // possible that another notification has already been reused for the resumption + // controls of this package. In this case, rather than renaming this player as + // packageName, just remove it and then send a update to the existing resumption + // controls. + notifyMediaDataRemoved(key) + notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated) + } + logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId) + + // Limit total number of resume controls + val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption } + val numResume = resumeEntries.size + if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { + resumeEntries + .toList() + .sortedBy { (key, data) -> data.lastActive } + .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) + .forEach { (key, data) -> + Log.d(TAG, "Removing excess control $key") + mediaEntries.remove(key) + notifyMediaDataRemoved(key) + logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) + } + } + } + + override fun setMediaResumptionEnabled(isEnabled: Boolean) { + if (useMediaResumption == isEnabled) { + return + } + + useMediaResumption = isEnabled + + if (!useMediaResumption) { + // Remove any existing resume controls + val filtered = mediaEntries.filter { !it.value.active } + filtered.forEach { + mediaEntries.remove(it.key) + notifyMediaDataRemoved(it.key) + logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId) + } + } + } + + /** Invoked when the user has dismissed the media carousel */ + override fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss() + + /** Are there any media notifications active, including the recommendations? */ + override fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation() + + /** + * Are there any media entries we should display, including the recommendations? + * - If resumption is enabled, this will include inactive players + * - If resumption is disabled, we only want to show active players + */ + override fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation() + + /** Are there any resume media notifications active, excluding the recommendations? */ + override fun hasActiveMedia() = mediaDataFilter.hasActiveMedia() + + /** + * Are there any resume media notifications active, excluding the recommendations? + * - If resumption is enabled, this will include inactive players + * - If resumption is disabled, we only want to show active players + */ + override fun hasAnyMedia() = mediaDataFilter.hasAnyMedia() + override fun isRecommendationActive() = smartspaceMediaData.isActive + + /** + * Converts the pass-in SmartspaceTarget to SmartspaceMediaData + * + * @return An empty SmartspaceMediaData with the valid target Id is returned if the + * SmartspaceTarget's data is invalid. + */ + private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData { + val baseAction: SmartspaceAction? = target.baseAction + val dismissIntent = + baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? + + val isActive = + when { + !mediaFlags.isPersistentSsCardEnabled() -> true + baseAction == null -> true + else -> { + val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE) + triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC + } + } + + packageName(target)?.let { + return SmartspaceMediaData( + targetId = target.smartspaceTargetId, + isActive = isActive, + packageName = it, + cardAction = target.baseAction, + recommendations = target.iconGrid, + dismissIntent = dismissIntent, + headphoneConnectionTimeMillis = target.creationTimeMillis, + instanceId = logger.getNewInstanceId(), + expiryTimeMs = target.expiryTimeMillis, + ) + } + return EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = target.smartspaceTargetId, + isActive = isActive, + dismissIntent = dismissIntent, + headphoneConnectionTimeMillis = target.creationTimeMillis, + instanceId = logger.getNewInstanceId(), + expiryTimeMs = target.expiryTimeMillis, + ) + } + + private fun packageName(target: SmartspaceTarget): String? { + val recommendationList = target.iconGrid + if (recommendationList == null || recommendationList.isEmpty()) { + Log.w(TAG, "Empty or null media recommendation list.") + return null + } + for (recommendation in recommendationList) { + val extras = recommendation.extras + extras?.let { + it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName -> + return packageName + } + } + } + Log.w(TAG, "No valid package name is provided.") + return null + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.apply { + println("internalListeners: $internalListeners") + println("externalListeners: ${mediaDataFilter.listeners}") + println("mediaEntries: $mediaEntries") + println("useMediaResumption: $useMediaResumption") + println("allowMediaRecommendations: $allowMediaRecommendations") + } + mediaDeviceManager.dump(pw) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt new file mode 100644 index 000000000000..a65db35030ea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.domain.pipeline + +import android.content.Context +import android.content.pm.UserInfo +import android.os.SystemProperties +import android.util.Log +import com.android.internal.annotations.KeepForWeakReference +import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.android.systemui.util.time.SystemClock +import java.util.SortedMap +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +private const val TAG = "MediaDataFilter" +private const val DEBUG = true +private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = + ("com.google" + + ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity") +private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds" + +/** + * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user + * switches (removing entries for the previous user, adding back entries for the current user). Also + * filters out smartspace updates in favor of local recent media, when avaialble. + * + * This is added at the end of the pipeline since we may still need to handle callbacks from + * background users (e.g. timeouts). + */ +class MediaDataFilterImpl +@Inject +constructor( + private val context: Context, + userTracker: UserTracker, + private val broadcastSender: BroadcastSender, + private val lockscreenUserManager: NotificationLockscreenUserManager, + @Main private val executor: Executor, + private val systemClock: SystemClock, + private val logger: MediaUiEventLogger, + private val mediaFlags: MediaFlags, + private val mediaFilterRepository: MediaFilterRepository, +) : MediaDataManager.Listener { + private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() + val listeners: Set<MediaDataManager.Listener> + get() = _listeners.toSet() + lateinit var mediaDataManager: MediaDataManager + + // Ensure the field (and associated reference) isn't removed during optimization. + @KeepForWeakReference + private val userTrackerCallback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + handleUserSwitched() + } + + override fun onProfilesChanged(profiles: List<UserInfo>) { + handleProfileChanged() + } + } + + init { + userTracker.addCallback(userTrackerCallback, executor) + } + + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean, + receivedSmartspaceCardLatency: Int, + isSsReactivated: Boolean + ) { + if (oldKey != null && oldKey != key) { + mediaFilterRepository.removeMediaEntry(oldKey) + } + mediaFilterRepository.addMediaEntry(key, data) + + if ( + !lockscreenUserManager.isCurrentProfile(data.userId) || + !lockscreenUserManager.isProfileAvailable(data.userId) + ) { + return + } + + if (oldKey != null && oldKey != key) { + mediaFilterRepository.removeSelectedUserMediaEntry(oldKey) + } + mediaFilterRepository.addSelectedUserMediaEntry(key, data) + + // Notify listeners + listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) } + } + + override fun onSmartspaceMediaDataLoaded( + key: String, + data: SmartspaceMediaData, + shouldPrioritize: Boolean + ) { + // With persistent recommendation card, we could get a background update while inactive + // Otherwise, consider it an invalid update + if (!data.isActive && !mediaFlags.isPersistentSsCardEnabled()) { + Log.d(TAG, "Inactive recommendation data. Skip triggering.") + return + } + + // Override the pass-in value here, as the order of Smartspace card is only determined here. + var shouldPrioritizeMutable = false + mediaFilterRepository.setRecommendation(data) + + // Before forwarding the smartspace target, first check if we have recently inactive media + val selectedUserEntries = mediaFilterRepository.selectedUserEntries.value + val sorted = + selectedUserEntries.toSortedMap(compareBy { selectedUserEntries[it]?.lastActive ?: -1 }) + val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted) + var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE + data.cardAction?.extras?.let { + val smartspaceMaxAgeSeconds = it.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0) + if (smartspaceMaxAgeSeconds > 0) { + smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds) + } + } + + // Check if smartspace has explicitly specified whether to re-activate resumable media. + // The default behavior is to trigger if the smartspace data is active. + val shouldTriggerResume = + data.cardAction?.extras?.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) ?: true + val shouldReactivate = + shouldTriggerResume && + !selectedUserEntries.any { it.value.active } && + selectedUserEntries.isNotEmpty() && + data.isActive + + if (timeSinceActive < smartspaceMaxAgeMillis) { + // It could happen there are existing active media resume cards, then we don't need to + // reactivate. + if (shouldReactivate) { + val lastActiveKey = sorted.lastKey() // most recently active + // Notify listeners to consider this media active + Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") + mediaFilterRepository.setReactivatedKey(lastActiveKey) + val mediaData = sorted[lastActiveKey]!!.copy(active = true) + logger.logRecommendationActivated( + mediaData.appUid, + mediaData.packageName, + mediaData.instanceId + ) + listeners.forEach { + it.onMediaDataLoaded( + lastActiveKey, + lastActiveKey, + mediaData, + receivedSmartspaceCardLatency = + (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) + .toInt(), + isSsReactivated = true + ) + } + } + } else if (data.isActive) { + // Mark to prioritize Smartspace card if no recent media. + shouldPrioritizeMutable = true + } + + if (!data.isValid()) { + Log.d(TAG, "Invalid recommendation data. Skip showing the rec card") + return + } + val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value + logger.logRecommendationAdded( + smartspaceMediaData.packageName, + smartspaceMediaData.instanceId + ) + listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } + } + + override fun onMediaDataRemoved(key: String) { + mediaFilterRepository.removeMediaEntry(key) + mediaFilterRepository.removeSelectedUserMediaEntry(key)?.let { + // Only notify listeners if something actually changed + listeners.forEach { it.onMediaDataRemoved(key) } + } + } + + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + // First check if we had reactivated media instead of forwarding smartspace + mediaFilterRepository.reactivatedKey.value?.let { + val lastActiveKey = it + mediaFilterRepository.setReactivatedKey(null) + Log.d(TAG, "expiring reactivated key $lastActiveKey") + // Notify listeners to update with actual active value + mediaFilterRepository.selectedUserEntries.value[lastActiveKey]?.let { mediaData -> + listeners.forEach { listener -> + listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately) + } + } + } + + val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value + if (smartspaceMediaData.isActive) { + mediaFilterRepository.setRecommendation( + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) + ) + } + listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } + } + + @VisibleForTesting + internal fun handleProfileChanged() { + // TODO(b/317221348) re-add media removed when profile is available. + mediaFilterRepository.allUserEntries.value.forEach { (key, data) -> + if (!lockscreenUserManager.isProfileAvailable(data.userId)) { + // Only remove media when the profile is unavailable. + if (DEBUG) Log.d(TAG, "Removing $key after profile change") + mediaFilterRepository.removeSelectedUserMediaEntry(key, data) + listeners.forEach { listener -> listener.onMediaDataRemoved(key) } + } + } + } + + @VisibleForTesting + internal fun handleUserSwitched() { + // If the user changes, remove all current MediaData objects and inform listeners + val listenersCopy = listeners + val keyCopy = mediaFilterRepository.selectedUserEntries.value.keys.toMutableList() + // Clear the list first, to make sure callbacks from listeners if we have any entries + // are up to date + mediaFilterRepository.clearSelectedUserMedia() + keyCopy.forEach { + if (DEBUG) Log.d(TAG, "Removing $it after user change") + listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) } + } + + mediaFilterRepository.allUserEntries.value.forEach { (key, data) -> + if (lockscreenUserManager.isCurrentProfile(data.userId)) { + if (DEBUG) Log.d(TAG, "Re-adding $key after user change") + mediaFilterRepository.addSelectedUserMediaEntry(key, data) + listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } + } + } + } + + /** Invoked when the user has dismissed the media carousel */ + fun onSwipeToDismiss() { + if (DEBUG) Log.d(TAG, "Media carousel swiped away") + val mediaKeys = mediaFilterRepository.selectedUserEntries.value.keys.toSet() + mediaKeys.forEach { + // Force updates to listeners, needed for re-activated card + mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true) + } + val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value + if (smartspaceMediaData.isActive) { + val dismissIntent = smartspaceMediaData.dismissIntent + if (dismissIntent == null) { + Log.w( + TAG, + "Cannot create dismiss action click action: extras missing dismiss_intent." + ) + } else if ( + dismissIntent.component?.className == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME + ) { + // Dismiss the card Smartspace data through Smartspace trampoline activity. + context.startActivity(dismissIntent) + } else { + broadcastSender.sendBroadcast(dismissIntent) + } + + if (mediaFlags.isPersistentSsCardEnabled()) { + mediaFilterRepository.setRecommendation(smartspaceMediaData.copy(isActive = false)) + mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId) + } else { + mediaFilterRepository.setRecommendation( + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId, + ) + ) + mediaDataManager.dismissSmartspaceRecommendation( + smartspaceMediaData.targetId, + delay = 0L, + ) + } + } + } + + /** Add a listener for filtered [MediaData] changes */ + fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) + + /** Remove a listener that was registered with addListener */ + fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) + + /** + * Return the time since last active for the most-recent media. + * + * @param sortedEntries selectedUserEntries sorted from the earliest to the most-recent. + * @return The duration in milliseconds from the most-recent media's last active timestamp to + * the present. MAX_VALUE will be returned if there is no media. + */ + private fun timeSinceActiveForMostRecentMedia( + sortedEntries: SortedMap<String, MediaData> + ): Long { + if (sortedEntries.isEmpty()) { + return Long.MAX_VALUE + } + + val now = systemClock.elapsedRealtime() + val lastActiveKey = sortedEntries.lastKey() // most recently active + return sortedEntries[lastActiveKey]?.let { now - it.lastActive } ?: Long.MAX_VALUE + } + + companion object { + /** + * Maximum age of a media control to re-activate on smartspace signal. If there is no media + * control available within this time window, smartspace recommendations will be shown + * instead. + */ + @VisibleForTesting + internal val SMARTSPACE_MAX_AGE: Long + get() = + SystemProperties.getLong( + "debug.sysui.smartspace_max_age", + TimeUnit.MINUTES.toMillis(30) + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt index 865c49e1d817..2b1070cfeedf 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,554 +16,21 @@ package com.android.systemui.media.controls.domain.pipeline -import android.annotation.SuppressLint -import android.app.ActivityOptions -import android.app.BroadcastOptions -import android.app.Notification -import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME import android.app.PendingIntent -import android.app.StatusBarManager -import android.app.UriGrantsManager -import android.app.smartspace.SmartspaceAction -import android.app.smartspace.SmartspaceConfig -import android.app.smartspace.SmartspaceManager -import android.app.smartspace.SmartspaceSession -import android.app.smartspace.SmartspaceTarget -import android.content.BroadcastReceiver -import android.content.ContentProvider -import android.content.ContentResolver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.ImageDecoder -import android.graphics.drawable.Animatable -import android.graphics.drawable.Icon import android.media.MediaDescription -import android.media.MediaMetadata -import android.media.session.MediaController import android.media.session.MediaSession -import android.media.session.PlaybackState -import android.net.Uri -import android.os.Parcelable -import android.os.Process -import android.os.UserHandle -import android.provider.Settings import android.service.notification.StatusBarNotification -import android.support.v4.media.MediaMetadataCompat -import android.text.TextUtils -import android.util.Log -import android.util.Pair as APair -import androidx.media.utils.MediaConstants -import com.android.app.tracing.traceSection -import com.android.internal.annotations.Keep -import com.android.internal.logging.InstanceId -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.systemui.Dumpable -import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager -import com.android.systemui.media.controls.domain.resume.MediaResumeListener -import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser -import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE -import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC -import com.android.systemui.media.controls.shared.model.MediaAction -import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData -import com.android.systemui.media.controls.shared.model.MediaDeviceData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData -import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider -import com.android.systemui.media.controls.ui.view.MediaViewHolder -import com.android.systemui.media.controls.util.MediaControllerFactory -import com.android.systemui.media.controls.util.MediaDataUtils -import com.android.systemui.media.controls.util.MediaFlags -import com.android.systemui.media.controls.util.MediaUiEventLogger -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.BcSmartspaceDataPlugin -import com.android.systemui.res.R -import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState -import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState -import com.android.systemui.statusbar.notification.row.HybridGroupManager -import com.android.systemui.tuner.TunerService -import com.android.systemui.util.Assert -import com.android.systemui.util.Utils -import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.concurrency.ThreadFactory -import com.android.systemui.util.time.SystemClock -import java.io.IOException -import java.io.PrintWriter -import java.util.concurrent.Executor -import javax.inject.Inject -// URI fields to try loading album art from -private val ART_URIS = - arrayOf( - MediaMetadata.METADATA_KEY_ALBUM_ART_URI, - MediaMetadata.METADATA_KEY_ART_URI, - MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI - ) - -private const val TAG = "MediaDataManager" -private const val DEBUG = true -private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent" - -private val LOADING = - MediaData( - userId = -1, - initialized = false, - app = null, - appIcon = null, - artist = null, - song = null, - artwork = null, - actions = emptyList(), - actionsToShowInCompact = emptyList(), - packageName = "INVALID", - token = null, - clickIntent = null, - device = null, - active = true, - resumeAction = null, - instanceId = InstanceId.fakeInstanceId(-1), - appUid = Process.INVALID_UID - ) - -internal val EMPTY_SMARTSPACE_MEDIA_DATA = - SmartspaceMediaData( - targetId = "INVALID", - isActive = false, - packageName = "INVALID", - cardAction = null, - recommendations = emptyList(), - dismissIntent = null, - headphoneConnectionTimeMillis = 0, - instanceId = InstanceId.fakeInstanceId(-1), - expiryTimeMs = 0, - ) - -const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank." - -fun isMediaNotification(sbn: StatusBarNotification): Boolean { - return sbn.notification.isMediaNotification() -} - -/** - * Allow recommendations from smartspace to show in media controls. Requires - * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0 - */ -private fun allowMediaRecommendations(context: Context): Boolean { - val flag = - Settings.Secure.getInt( - context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, - 1 - ) - return Utils.useQsMediaPlayer(context) && flag > 0 -} - -/** A class that facilitates management and loading of Media Data, ready for binding. */ -@SysUISingleton -class MediaDataManager( - private val context: Context, - @Background private val backgroundExecutor: Executor, - @Main private val uiExecutor: Executor, - @Main private val foregroundExecutor: DelayableExecutor, - private val mediaControllerFactory: MediaControllerFactory, - private val broadcastDispatcher: BroadcastDispatcher, - dumpManager: DumpManager, - mediaTimeoutListener: MediaTimeoutListener, - mediaResumeListener: MediaResumeListener, - mediaSessionBasedFilter: MediaSessionBasedFilter, - mediaDeviceManager: MediaDeviceManager, - mediaDataCombineLatest: MediaDataCombineLatest, - private val mediaDataFilter: MediaDataFilter, - private val activityStarter: ActivityStarter, - private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider, - private var useMediaResumption: Boolean, - private val useQsMediaPlayer: Boolean, - private val systemClock: SystemClock, - private val tunerService: TunerService, - private val mediaFlags: MediaFlags, - private val logger: MediaUiEventLogger, - private val smartspaceManager: SmartspaceManager?, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, -) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener { - - companion object { - // UI surface label for subscribing Smartspace updates. - @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" - - // Smartspace package name's extra key. - @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" - - // Maximum number of actions allowed in compact view - @JvmField val MAX_COMPACT_ACTIONS = 3 - - // Maximum number of actions allowed in expanded view - @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size - } - - private val themeText = - com.android.settingslib.Utils.getColorAttr( - context, - com.android.internal.R.attr.textColorPrimary - ) - .defaultColor - - // Internal listeners are part of the internal pipeline. External listeners (those registered - // with [MediaDeviceManager.addListener]) receive events after they have propagated through - // the internal pipeline. - // Another way to think of the distinction between internal and external listeners is the - // following. Internal listeners are listeners that MediaDataManager depends on, and external - // listeners are listeners that depend on MediaDataManager. - // TODO(b/159539991#comment5): Move internal listeners to separate package. - private val internalListeners: MutableSet<Listener> = mutableSetOf() - private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() - // There should ONLY be at most one Smartspace media recommendation. - var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA - @Keep private var smartspaceSession: SmartspaceSession? = null - private var allowMediaRecommendations = allowMediaRecommendations(context) - - private val artworkWidth = - context.resources.getDimensionPixelSize( - com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize - ) - private val artworkHeight = - context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) - - @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE - private val statusBarManager = - context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager - - /** Check whether this notification is an RCN */ - private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean { - return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) - } - - @Inject - constructor( - context: Context, - threadFactory: ThreadFactory, - @Main uiExecutor: Executor, - @Main foregroundExecutor: DelayableExecutor, - mediaControllerFactory: MediaControllerFactory, - dumpManager: DumpManager, - broadcastDispatcher: BroadcastDispatcher, - mediaTimeoutListener: MediaTimeoutListener, - mediaResumeListener: MediaResumeListener, - mediaSessionBasedFilter: MediaSessionBasedFilter, - mediaDeviceManager: MediaDeviceManager, - mediaDataCombineLatest: MediaDataCombineLatest, - mediaDataFilter: MediaDataFilter, - activityStarter: ActivityStarter, - smartspaceMediaDataProvider: SmartspaceMediaDataProvider, - clock: SystemClock, - tunerService: TunerService, - mediaFlags: MediaFlags, - logger: MediaUiEventLogger, - smartspaceManager: SmartspaceManager?, - keyguardUpdateMonitor: KeyguardUpdateMonitor, - ) : this( - context, - // Loading bitmap for UMO background can take longer time, so it cannot run on the default - // background thread. Use a custom thread for media. - threadFactory.buildExecutorOnNewThread(TAG), - uiExecutor, - foregroundExecutor, - mediaControllerFactory, - broadcastDispatcher, - dumpManager, - mediaTimeoutListener, - mediaResumeListener, - mediaSessionBasedFilter, - mediaDeviceManager, - mediaDataCombineLatest, - mediaDataFilter, - activityStarter, - smartspaceMediaDataProvider, - Utils.useMediaResumption(context), - Utils.useQsMediaPlayer(context), - clock, - tunerService, - mediaFlags, - logger, - smartspaceManager, - keyguardUpdateMonitor, - ) - - private val appChangeReceiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - when (intent.action) { - Intent.ACTION_PACKAGES_SUSPENDED -> { - val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) - packages?.forEach { removeAllForPackage(it) } - } - Intent.ACTION_PACKAGE_REMOVED, - Intent.ACTION_PACKAGE_RESTARTED -> { - intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) } - } - } - } - } - - init { - dumpManager.registerDumpable(TAG, this) - - // Initialize the internal processing pipeline. The listeners at the front of the pipeline - // are set as internal listeners so that they receive events. From there, events are - // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, - // so it is responsible for dispatching events to external listeners. To achieve this, - // external listeners that are registered with [MediaDataManager.addListener] are actually - // registered as listeners to mediaDataFilter. - addInternalListener(mediaTimeoutListener) - addInternalListener(mediaResumeListener) - addInternalListener(mediaSessionBasedFilter) - mediaSessionBasedFilter.addListener(mediaDeviceManager) - mediaSessionBasedFilter.addListener(mediaDataCombineLatest) - mediaDeviceManager.addListener(mediaDataCombineLatest) - mediaDataCombineLatest.addListener(mediaDataFilter) - - // Set up links back into the pipeline for listeners that need to send events upstream. - mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> - setTimedOut(key, timedOut) - } - mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> - updateState(key, state) - } - mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) } - mediaResumeListener.setManager(this) - mediaDataFilter.mediaDataManager = this - - val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) - broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) - - val uninstallFilter = - IntentFilter().apply { - addAction(Intent.ACTION_PACKAGE_REMOVED) - addAction(Intent.ACTION_PACKAGE_RESTARTED) - addDataScheme("package") - } - // BroadcastDispatcher does not allow filters with data schemes - context.registerReceiver(appChangeReceiver, uninstallFilter) - - // Register for Smartspace data updates. - smartspaceMediaDataProvider.registerListener(this) - smartspaceSession = - smartspaceManager?.createSmartspaceSession( - SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() - ) - smartspaceSession?.let { - it.addOnTargetsAvailableListener( - // Use a main uiExecutor thread listening to Smartspace updates instead of using - // the existing background executor. - // SmartspaceSession has scheduled routine updates which can be unpredictable on - // test simulators, using the backgroundExecutor makes it's hard to test the threads - // numbers. - uiExecutor, - SmartspaceSession.OnTargetsAvailableListener { targets -> - smartspaceMediaDataProvider.onTargetsAvailable(targets) - } - ) - } - smartspaceSession?.let { it.requestSmartspaceUpdate() } - tunerService.addTunable( - object : TunerService.Tunable { - override fun onTuningChanged(key: String?, newValue: String?) { - allowMediaRecommendations = allowMediaRecommendations(context) - if (!allowMediaRecommendations) { - dismissSmartspaceRecommendation( - key = smartspaceMediaData.targetId, - delay = 0L - ) - } - } - }, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION - ) - } - - fun destroy() { - smartspaceMediaDataProvider.unregisterListener(this) - smartspaceSession?.close() - smartspaceSession = null - context.unregisterReceiver(appChangeReceiver) - } - - fun onNotificationAdded(key: String, sbn: StatusBarNotification) { - if (useQsMediaPlayer && isMediaNotification(sbn)) { - var isNewlyActiveEntry = false - Assert.isMainThread() - val oldKey = findExistingEntry(key, sbn.packageName) - if (oldKey == null) { - val instanceId = logger.getNewInstanceId() - val temp = - LOADING.copy( - packageName = sbn.packageName, - instanceId = instanceId, - createdTimestampMillis = systemClock.currentTimeMillis(), - ) - mediaEntries.put(key, temp) - isNewlyActiveEntry = true - } else if (oldKey != key) { - // Resume -> active conversion; move to new key - val oldData = mediaEntries.remove(oldKey)!! - isNewlyActiveEntry = true - mediaEntries.put(key, oldData) - } - loadMediaData(key, sbn, oldKey, isNewlyActiveEntry) - } else { - onNotificationRemoved(key) - } - } - - private fun removeAllForPackage(packageName: String) { - Assert.isMainThread() - val toRemove = mediaEntries.filter { it.value.packageName == packageName } - toRemove.forEach { removeEntry(it.key) } - } - - fun setResumeAction(key: String, action: Runnable?) { - mediaEntries.get(key)?.let { - it.resumeAction = action - it.hasCheckedForResume = true - } - } - - fun addResumptionControls( - userId: Int, - desc: MediaDescription, - action: Runnable, - token: MediaSession.Token, - appName: String, - appIntent: PendingIntent, - packageName: String - ) { - // Resume controls don't have a notification key, so store by package name instead - if (!mediaEntries.containsKey(packageName)) { - val instanceId = logger.getNewInstanceId() - val appUid = - try { - context.packageManager.getApplicationInfo(packageName, 0)?.uid!! - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Could not get app UID for $packageName", e) - Process.INVALID_UID - } - - val resumeData = - LOADING.copy( - packageName = packageName, - resumeAction = action, - hasCheckedForResume = true, - instanceId = instanceId, - appUid = appUid, - createdTimestampMillis = systemClock.currentTimeMillis(), - ) - mediaEntries.put(packageName, resumeData) - logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) - logger.logResumeMediaAdded(appUid, packageName, instanceId) - } - backgroundExecutor.execute { - loadMediaDataInBgForResumption( - userId, - desc, - action, - token, - appName, - appIntent, - packageName - ) - } - } - - /** - * Check if there is an existing entry that matches the key or package name. Returns the key - * that matches, or null if not found. - */ - private fun findExistingEntry(key: String, packageName: String): String? { - if (mediaEntries.containsKey(key)) { - return key - } - // Check if we already had a resume player - if (mediaEntries.containsKey(packageName)) { - return packageName - } - return null - } - - private fun loadMediaData( - key: String, - sbn: StatusBarNotification, - oldKey: String?, - isNewlyActiveEntry: Boolean = false, - ) { - backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } - } +/** Facilitates management and loading of Media Data, ready for binding. */ +interface MediaDataManager { /** Add a listener for changes in this class */ - fun addListener(listener: Listener) { - // mediaDataFilter is the current end of the internal pipeline. Register external - // listeners as listeners to it. - mediaDataFilter.addListener(listener) - } + fun addListener(listener: Listener) /** Remove a listener for changes in this class */ - fun removeListener(listener: Listener) { - // Since mediaDataFilter is the current end of the internal pipelie, external listeners - // have been registered to it. So, they need to be removed from it too. - mediaDataFilter.removeListener(listener) - } - - /** Add a listener for internal events. */ - private fun addInternalListener(listener: Listener) = internalListeners.add(listener) - - /** - * Notify internal listeners of media loaded event. - * - * External listeners registered with [addListener] will be notified after the event propagates - * through the internal listener pipeline. - */ - private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { - internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) } - } - - /** - * Notify internal listeners of Smartspace media loaded event. - * - * External listeners registered with [addListener] will be notified after the event propagates - * through the internal listener pipeline. - */ - private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { - internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) } - } - - /** - * Notify internal listeners of media removed event. - * - * External listeners registered with [addListener] will be notified after the event propagates - * through the internal listener pipeline. - */ - private fun notifyMediaDataRemoved(key: String) { - internalListeners.forEach { it.onMediaDataRemoved(key) } - } - - /** - * Notify internal listeners of Smartspace media removed event. - * - * External listeners registered with [addListener] will be notified after the event propagates - * through the internal listener pipeline. - * - * @param immediately indicates should apply the UI changes immediately, otherwise wait until - * the next refresh-round before UI becomes visible. Should only be true if the update is - * initiated by user's interaction. - */ - private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) { - internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } - } + fun removeListener(listener: Listener) /** * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This @@ -571,1055 +38,64 @@ class MediaDataManager( * * @see MediaData.active */ - internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) { - mediaEntries[key]?.let { - if (timedOut && !forceUpdate) { - // Only log this event when media expires on its own - logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId) - } - if (it.active == !timedOut && !forceUpdate) { - if (it.resumption) { - if (DEBUG) Log.d(TAG, "timing out resume player $key") - dismissMediaData(key, 0L /* delay */) - } - return - } - // Update last active if media was still active. - if (it.active) { - it.lastActive = systemClock.elapsedRealtime() - } - it.active = !timedOut - if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") - onMediaDataLoaded(key, key, it) - } + fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean = false) - if (key == smartspaceMediaData.targetId) { - if (DEBUG) Log.d(TAG, "smartspace card expired") - dismissSmartspaceRecommendation(key, delay = 0L) - } - } - - /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ - private fun updateState(key: String, state: PlaybackState) { - mediaEntries.get(key)?.let { - val token = it.token - if (token == null) { - if (DEBUG) Log.d(TAG, "State updated, but token was null") - return - } - val actions = - createActionsFromState( - it.packageName, - mediaControllerFactory.create(it.token), - UserHandle(it.userId) - ) + /** Invoked when media notification is added. */ + fun onNotificationAdded(key: String, sbn: StatusBarNotification) - // Control buttons - // If flag is enabled and controller has a PlaybackState, - // create actions from session info - // otherwise, no need to update semantic actions. - val data = - if (actions != null) { - it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) - } else { - it.copy(isPlaying = isPlayingState(state.state)) - } - if (DEBUG) Log.d(TAG, "State updated outside of notification") - onMediaDataLoaded(key, key, data) - } - } + fun destroy() - private fun removeEntry(key: String, logEvent: Boolean = true) { - mediaEntries.remove(key)?.let { - if (logEvent) { - logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) - } - } - notifyMediaDataRemoved(key) - } + /** Sets resume action. */ + fun setResumeAction(key: String, action: Runnable?) - /** Dismiss a media entry. Returns false if the key was not found. */ - fun dismissMediaData(key: String, delay: Long): Boolean { - val existed = mediaEntries[key] != null - backgroundExecutor.execute { - mediaEntries[key]?.let { mediaData -> - if (mediaData.isLocalSession()) { - mediaData.token?.let { - val mediaController = mediaControllerFactory.create(it) - mediaController.transportControls.stop() - } - } - } - } - foregroundExecutor.executeDelayed({ removeEntry(key) }, delay) - return existed - } - - /** - * Called whenever the recommendation has been expired or removed by the user. This will remove - * the recommendation card entirely from the carousel. - */ - fun dismissSmartspaceRecommendation(key: String, delay: Long) { - if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { - // If this doesn't match, or we've already invalidated the data, no action needed - return - } - - if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target") - if (smartspaceMediaData.isActive) { - smartspaceMediaData = - EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId - ) - } - foregroundExecutor.executeDelayed( - { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) }, - delay - ) - } - - /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ - fun setRecommendationInactive(key: String) { - if (!mediaFlags.isPersistentSsCardEnabled()) { - Log.e(TAG, "Only persistent recommendation can be inactive!") - return - } - if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive") - - if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { - // If this doesn't match, or we've already invalidated the data, no action needed - return - } - - smartspaceMediaData = smartspaceMediaData.copy(isActive = false) - notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) - } - - private fun loadMediaDataInBgForResumption( + /** Adds resume media data. */ + fun addResumptionControls( userId: Int, desc: MediaDescription, - resumeAction: Runnable, + action: Runnable, token: MediaSession.Token, appName: String, appIntent: PendingIntent, packageName: String - ) { - if (desc.title.isNullOrBlank()) { - Log.e(TAG, "Description incomplete") - // Delete the placeholder entry - mediaEntries.remove(packageName) - return - } - - if (DEBUG) { - Log.d(TAG, "adding track for $userId from browser: $desc") - } - - val currentEntry = mediaEntries.get(packageName) - val appUid = currentEntry?.appUid ?: Process.INVALID_UID - - // Album art - var artworkBitmap = desc.iconBitmap - if (artworkBitmap == null && desc.iconUri != null) { - artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName) - } - val artworkIcon = - if (artworkBitmap != null) { - Icon.createWithBitmap(artworkBitmap) - } else { - null - } - - val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() - val isExplicit = - desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == - MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT - - val progress = - if (mediaFlags.isResumeProgressEnabled()) { - MediaDataUtils.getDescriptionProgress(desc.extras) - } else null - - val mediaAction = getResumeMediaAction(resumeAction) - val lastActive = systemClock.elapsedRealtime() - val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L - foregroundExecutor.execute { - onMediaDataLoaded( - packageName, - null, - MediaData( - userId, - true, - appName, - null, - desc.subtitle, - desc.title, - artworkIcon, - listOf(mediaAction), - listOf(0), - MediaButton(playOrPause = mediaAction), - packageName, - token, - appIntent, - device = null, - active = false, - resumeAction = resumeAction, - resumption = true, - notificationKey = packageName, - hasCheckedForResume = true, - lastActive = lastActive, - createdTimestampMillis = createdTimestampMillis, - instanceId = instanceId, - appUid = appUid, - isExplicit = isExplicit, - resumeProgress = progress, - ) - ) - } - } - - fun loadMediaDataInBg( - key: String, - sbn: StatusBarNotification, - oldKey: String?, - isNewlyActiveEntry: Boolean = false, - ) { - val token = - sbn.notification.extras.getParcelable( - Notification.EXTRA_MEDIA_SESSION, - MediaSession.Token::class.java - ) - if (token == null) { - return - } - val mediaController = mediaControllerFactory.create(token) - val metadata = mediaController.metadata - val notif: Notification = sbn.notification - - val appInfo = - notif.extras.getParcelable( - Notification.EXTRA_BUILDER_APPLICATION_INFO, - ApplicationInfo::class.java - ) - ?: getAppInfoFromPackage(sbn.packageName) - - // App name - val appName = getAppName(sbn, appInfo) - - // Song name - var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) - if (song.isNullOrBlank()) { - song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) - } - if (song.isNullOrBlank()) { - song = HybridGroupManager.resolveTitle(notif) - } - if (song.isNullOrBlank()) { - // For apps that don't include a title, log and add a placeholder - song = context.getString(R.string.controls_media_empty_title, appName) - try { - statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier) - } catch (e: RuntimeException) { - Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}") - } - } - - // Album art - var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } - if (artworkBitmap == null) { - artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) - } - if (artworkBitmap == null) { - artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) - } - val artWorkIcon = - if (artworkBitmap == null) { - notif.getLargeIcon() - } else { - Icon.createWithBitmap(artworkBitmap) - } - - // App Icon - val smallIcon = sbn.notification.smallIcon - - // Explicit Indicator - var isExplicit = false - val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) - isExplicit = - mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == - MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT - - // Artist name - var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) - if (artist.isNullOrBlank()) { - artist = HybridGroupManager.resolveText(notif) - } - - // Device name (used for remote cast notifications) - var device: MediaDeviceData? = null - if (isRemoteCastNotification(sbn)) { - val extras = sbn.notification.extras - val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) - val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) - val deviceIntent = - extras.getParcelable( - Notification.EXTRA_MEDIA_REMOTE_INTENT, - PendingIntent::class.java - ) - Log.d(TAG, "$key is RCN for $deviceName") - - if (deviceName != null && deviceIcon > -1) { - // Name and icon must be present, but intent may be null - val enabled = deviceIntent != null && deviceIntent.isActivity - val deviceDrawable = - Icon.createWithResource(sbn.packageName, deviceIcon) - .loadDrawable(sbn.getPackageContext(context)) - device = - MediaDeviceData( - enabled, - deviceDrawable, - deviceName, - deviceIntent, - showBroadcastButton = false - ) - } - } - - // Control buttons - // If flag is enabled and controller has a PlaybackState, create actions from session info - // Otherwise, use the notification actions - var actionIcons: List<MediaAction> = emptyList() - var actionsToShowCollapsed: List<Int> = emptyList() - val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) - if (semanticActions == null) { - val actions = createActionsFromNotification(sbn) - actionIcons = actions.first - actionsToShowCollapsed = actions.second - } - - val playbackLocation = - if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE - else if ( - mediaController.playbackInfo?.playbackType == - MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL - ) - MediaData.PLAYBACK_LOCAL - else MediaData.PLAYBACK_CAST_LOCAL - val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null - - val currentEntry = mediaEntries.get(key) - val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() - val appUid = appInfo?.uid ?: Process.INVALID_UID - - if (isNewlyActiveEntry) { - logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) - logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) - } else if (playbackLocation != currentEntry?.playbackLocation) { - logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation) - } - - val lastActive = systemClock.elapsedRealtime() - val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L - foregroundExecutor.execute { - val resumeAction: Runnable? = mediaEntries[key]?.resumeAction - val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true - val active = mediaEntries[key]?.active ?: true - onMediaDataLoaded( - key, - oldKey, - MediaData( - sbn.normalizedUserId, - true, - appName, - smallIcon, - artist, - song, - artWorkIcon, - actionIcons, - actionsToShowCollapsed, - semanticActions, - sbn.packageName, - token, - notif.contentIntent, - device, - active, - resumeAction = resumeAction, - playbackLocation = playbackLocation, - notificationKey = key, - hasCheckedForResume = hasCheckedForResume, - isPlaying = isPlaying, - isClearable = !sbn.isOngoing, - lastActive = lastActive, - createdTimestampMillis = createdTimestampMillis, - instanceId = instanceId, - appUid = appUid, - isExplicit = isExplicit, - ) - ) - } - } - - private fun logSingleVsMultipleMediaAdded( - appUid: Int, - packageName: String, - instanceId: InstanceId - ) { - if (mediaEntries.size == 1) { - logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) - } else if (mediaEntries.size == 2) { - // Since this method is only called when there is a new media session added. - // logging needed once there is more than one media session in carousel. - logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId) - } - } - - private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { - try { - return context.packageManager.getApplicationInfo(packageName, 0) - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Could not get app info for $packageName", e) - } - return null - } - - private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { - val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) - if (name != null) { - return name - } - - return if (appInfo != null) { - context.packageManager.getApplicationLabel(appInfo).toString() - } else { - sbn.packageName - } - } - - /** Generate action buttons based on notification actions */ - private fun createActionsFromNotification( - sbn: StatusBarNotification - ): Pair<List<MediaAction>, List<Int>> { - val notif = sbn.notification - val actionIcons: MutableList<MediaAction> = ArrayList() - val actions = notif.actions - var actionsToShowCollapsed = - notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() - ?: mutableListOf() - if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { - Log.e( - TAG, - "Too many compact actions for ${sbn.key}," + - "limiting to first $MAX_COMPACT_ACTIONS" - ) - actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) - } - - if (actions != null) { - for ((index, action) in actions.withIndex()) { - if (index == MAX_NOTIFICATION_ACTIONS) { - Log.w( - TAG, - "Too many notification actions for ${sbn.key}," + - " limiting to first $MAX_NOTIFICATION_ACTIONS" - ) - break - } - if (action.getIcon() == null) { - if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}") - actionsToShowCollapsed.remove(index) - continue - } - val runnable = - if (action.actionIntent != null) { - Runnable { - if (action.actionIntent.isActivity) { - activityStarter.startPendingIntentDismissingKeyguard( - action.actionIntent - ) - } else if (action.isAuthenticationRequired()) { - activityStarter.dismissKeyguardThenExecute( - { - var result = sendPendingIntent(action.actionIntent) - result - }, - {}, - true - ) - } else { - sendPendingIntent(action.actionIntent) - } - } - } else { - null - } - val mediaActionIcon = - if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { - Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) - } else { - action.getIcon() - } - .setTint(themeText) - .loadDrawable(context) - val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) - actionIcons.add(mediaAction) - } - } - return Pair(actionIcons, actionsToShowCollapsed) - } - - /** - * Generates action button info for this media session based on the PlaybackState - * - * @param packageName Package name for the media app - * @param controller MediaController for the current session - * @return a Pair consisting of a list of media actions, and a list of ints representing which - * - * ``` - * of those actions should be shown in the compact player - * ``` - */ - private fun createActionsFromState( - packageName: String, - controller: MediaController, - user: UserHandle - ): MediaButton? { - val state = controller.playbackState - if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { - return null - } - - // First, check for standard actions - val playOrPause = - if (isConnectingState(state.state)) { - // Spinner needs to be animating to render anything. Start it here. - val drawable = - context.getDrawable(com.android.internal.R.drawable.progress_small_material) - (drawable as Animatable).start() - MediaAction( - drawable, - null, // no action to perform when clicked - context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), - // Specify a rebind id to prevent the spinner from restarting on later binds. - com.android.internal.R.drawable.progress_small_material - ) - } else if (isPlayingState(state.state)) { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) - } else { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) - } - val prevButton = - getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) - val nextButton = - getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) - - // Then, create a way to build any custom actions that will be needed - val customActions = - state.customActions - .asSequence() - .filterNotNull() - .map { getCustomAction(state, packageName, controller, it) } - .iterator() - fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null - - // Finally, assign the remaining button slots: play/pause A B C D - // A = previous, else custom action (if not reserved) - // B = next, else custom action (if not reserved) - // C and D are always custom actions - val reservePrev = - controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV - ) == true - val reserveNext = - controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT - ) == true - - val prevOrCustom = - if (prevButton != null) { - prevButton - } else if (!reservePrev) { - nextCustomAction() - } else { - null - } - - val nextOrCustom = - if (nextButton != null) { - nextButton - } else if (!reserveNext) { - nextCustomAction() - } else { - null - } - - return MediaButton( - playOrPause, - nextOrCustom, - prevOrCustom, - nextCustomAction(), - nextCustomAction(), - reserveNext, - reservePrev - ) - } - - /** - * Create a [MediaAction] for a given action and media session - * - * @param controller MediaController for the session - * @param stateActions The actions included with the session's [PlaybackState] - * @param action A [PlaybackState.Actions] value representing what action to generate. One of: - * ``` - * [PlaybackState.ACTION_PLAY] - * [PlaybackState.ACTION_PAUSE] - * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] - * [PlaybackState.ACTION_SKIP_TO_NEXT] - * @return - * ``` - * - * A [MediaAction] with correct values set, or null if the state doesn't support it - */ - private fun getStandardAction( - controller: MediaController, - stateActions: Long, - @PlaybackState.Actions action: Long - ): MediaAction? { - if (!includesAction(stateActions, action)) { - return null - } - - return when (action) { - PlaybackState.ACTION_PLAY -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_play), - { controller.transportControls.play() }, - context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container) - ) - } - PlaybackState.ACTION_PAUSE -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_pause), - { controller.transportControls.pause() }, - context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container) - ) - } - PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_prev), - { controller.transportControls.skipToPrevious() }, - context.getString(R.string.controls_media_button_prev), - null - ) - } - PlaybackState.ACTION_SKIP_TO_NEXT -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_next), - { controller.transportControls.skipToNext() }, - context.getString(R.string.controls_media_button_next), - null - ) - } - else -> null - } - } - - /** Check whether the actions from a [PlaybackState] include a specific action */ - private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { - if ( - (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && - (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) - ) { - return true - } - return (stateActions and action != 0L) - } - - /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ - private fun getCustomAction( - state: PlaybackState, - packageName: String, - controller: MediaController, - customAction: PlaybackState.CustomAction - ): MediaAction { - return MediaAction( - Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), - { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, - customAction.name, - null - ) - } - - /** Load a bitmap from the various Art metadata URIs */ - private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { - for (uri in ART_URIS) { - val uriString = metadata.getString(uri) - if (!TextUtils.isEmpty(uriString)) { - val albumArt = loadBitmapFromUri(Uri.parse(uriString)) - if (albumArt != null) { - if (DEBUG) Log.d(TAG, "loaded art from $uri") - return albumArt - } - } - } - return null - } - - private fun sendPendingIntent(intent: PendingIntent): Boolean { - return try { - val options = BroadcastOptions.makeBasic() - options.setInteractive(true) - options.setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED - ) - intent.send(options.toBundle()) - true - } catch (e: PendingIntent.CanceledException) { - Log.d(TAG, "Intent canceled", e) - false - } - } - - /** Returns a bitmap if the user can access the given URI, else null */ - private fun loadBitmapFromUriForUser( - uri: Uri, - userId: Int, - appUid: Int, - packageName: String, - ): Bitmap? { - try { - val ugm = UriGrantsManager.getService() - ugm.checkGrantUriPermission_ignoreNonSystem( - appUid, - packageName, - ContentProvider.getUriWithoutUserId(uri), - Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(uri, userId) - ) - return loadBitmapFromUri(uri) - } catch (e: SecurityException) { - Log.e(TAG, "Failed to get URI permission: $e") - } - return null - } - - /** - * Load a bitmap from a URI - * - * @param uri the uri to load - * @return bitmap, or null if couldn't be loaded - */ - private fun loadBitmapFromUri(uri: Uri): Bitmap? { - // ImageDecoder requires a scheme of the following types - if (uri.scheme == null) { - return null - } - - if ( - !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && - !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) && - !uri.scheme.equals(ContentResolver.SCHEME_FILE) - ) { - return null - } - - val source = ImageDecoder.createSource(context.contentResolver, uri) - return try { - ImageDecoder.decodeBitmap(source) { decoder, info, _ -> - val width = info.size.width - val height = info.size.height - val scale = - MediaDataUtils.getScaleFactor( - APair(width, height), - APair(artworkWidth, artworkHeight) - ) - - // Downscale if needed - if (scale != 0f && scale < 1) { - decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt()) - } - decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE - } - } catch (e: IOException) { - Log.e(TAG, "Unable to load bitmap", e) - null - } catch (e: RuntimeException) { - Log.e(TAG, "Unable to load bitmap", e) - null - } - } - - private fun getResumeMediaAction(action: Runnable): MediaAction { - return MediaAction( - Icon.createWithResource(context, R.drawable.ic_media_play) - .setTint(themeText) - .loadDrawable(context), - action, - context.getString(R.string.controls_media_resume), - context.getDrawable(R.drawable.ic_media_play_container) - ) - } - - fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = - traceSection("MediaDataManager#onMediaDataLoaded") { - Assert.isMainThread() - if (mediaEntries.containsKey(key)) { - // Otherwise this was removed already - mediaEntries.put(key, data) - notifyMediaDataLoaded(key, oldKey, data) - } - } - - override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { - if (!allowMediaRecommendations) { - if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.") - return - } - - val mediaTargets = targets.filterIsInstance<SmartspaceTarget>() - when (mediaTargets.size) { - 0 -> { - if (!smartspaceMediaData.isActive) { - return - } - if (DEBUG) { - Log.d(TAG, "Set Smartspace media to be inactive for the data update") - } - if (mediaFlags.isPersistentSsCardEnabled()) { - // Smartspace uses this signal to hide the card (e.g. when it expires or user - // disconnects headphones), so treat as setting inactive when flag is on - smartspaceMediaData = smartspaceMediaData.copy(isActive = false) - notifySmartspaceMediaDataLoaded( - smartspaceMediaData.targetId, - smartspaceMediaData, - ) - } else { - smartspaceMediaData = - EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId, - ) - notifySmartspaceMediaDataRemoved( - smartspaceMediaData.targetId, - immediately = false, - ) - } - } - 1 -> { - val newMediaTarget = mediaTargets.get(0) - if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) { - // The same Smartspace updates can be received. Skip the duplicate updates. - return - } - if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") - smartspaceMediaData = toSmartspaceMediaData(newMediaTarget) - notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) - } - else -> { - // There should NOT be more than 1 Smartspace media update. When it happens, it - // indicates a bad state or an error. Reset the status accordingly. - Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") - notifySmartspaceMediaDataRemoved( - smartspaceMediaData.targetId, - immediately = false, - ) - smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA - } - } - } - - fun onNotificationRemoved(key: String) { - Assert.isMainThread() - val removed = mediaEntries.remove(key) ?: return - if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { - logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) - } else if (isAbleToResume(removed)) { - convertToResumePlayer(key, removed) - } else if (mediaFlags.isRetainingPlayersEnabled()) { - handlePossibleRemoval(key, removed, notificationRemoved = true) - } else { - notifyMediaDataRemoved(key) - logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) - } - } - - private fun onSessionDestroyed(key: String) { - if (DEBUG) Log.d(TAG, "session destroyed for $key") - val entry = mediaEntries.remove(key) ?: return - // Clear token since the session is no longer valid - val updated = entry.copy(token = null) - handlePossibleRemoval(key, updated) - } + ) - private fun isAbleToResume(data: MediaData): Boolean { - val isEligibleForResume = - data.isLocalSession() || - (mediaFlags.isRemoteResumeAllowed() && - data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) - return useMediaResumption && data.resumeAction != null && isEligibleForResume - } + /** Dismiss a media entry. Returns false if the key was not found. */ + fun dismissMediaData(key: String, delay: Long): Boolean /** - * Convert to resume state if the player is no longer valid and active, then notify listeners - * that the data was updated. Does not convert to resume state if the player is still valid, or - * if it was removed before becoming inactive. (Assumes that [removed] was removed from - * [mediaEntries] before this function was called) + * Called whenever the recommendation has been expired or removed by the user. This will remove + * the recommendation card entirely from the carousel. */ - private fun handlePossibleRemoval( - key: String, - removed: MediaData, - notificationRemoved: Boolean = false - ) { - val hasSession = removed.token != null - if (hasSession && removed.semanticActions != null) { - // The app was using session actions, and the session is still valid: keep player - if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key") - mediaEntries.put(key, removed) - notifyMediaDataLoaded(key, key, removed) - } else if (!notificationRemoved && removed.semanticActions == null) { - // The app was using notification actions, and notif wasn't removed yet: keep player - if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key") - mediaEntries.put(key, removed) - notifyMediaDataLoaded(key, key, removed) - } else if (removed.active && !isAbleToResume(removed)) { - // This player was still active - it didn't last long enough to time out, - // and its app doesn't normally support resume: remove - if (DEBUG) Log.d(TAG, "Removing still-active player $key") - notifyMediaDataRemoved(key) - logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) - } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) { - // Convert to resume - if (DEBUG) { - Log.d( - TAG, - "Notification ($notificationRemoved) and/or session " + - "($hasSession) gone for inactive player $key" - ) - } - convertToResumePlayer(key, removed) - } else { - // Retaining players flag is off and app doesn't support resume: remove player. - if (DEBUG) Log.d(TAG, "Removing player $key") - notifyMediaDataRemoved(key) - logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) - } - } - - /** Set the given [MediaData] as a resume state player and notify listeners */ - private fun convertToResumePlayer(key: String, data: MediaData) { - if (DEBUG) Log.d(TAG, "Converting $key to resume") - // Resumption controls must have a title. - if (data.song.isNullOrBlank()) { - Log.e(TAG, "Description incomplete") - notifyMediaDataRemoved(key) - logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) - return - } - // Move to resume key (aka package name) if that key doesn't already exist. - val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) } - val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList() - val launcherIntent = - context.packageManager.getLaunchIntentForPackage(data.packageName)?.let { - PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE) - } - val lastActive = - if (data.active) { - systemClock.elapsedRealtime() - } else { - data.lastActive - } - val updated = - data.copy( - token = null, - actions = actions, - semanticActions = MediaButton(playOrPause = resumeAction), - actionsToShowInCompact = listOf(0), - active = false, - resumption = true, - isPlaying = false, - isClearable = true, - clickIntent = launcherIntent, - lastActive = lastActive, - ) - val pkg = data.packageName - val migrate = mediaEntries.put(pkg, updated) == null - // Notify listeners of "new" controls when migrating or removed and update when not - Log.d(TAG, "migrating? $migrate from $key -> $pkg") - if (migrate) { - notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated) - } else { - // Since packageName is used for the key of the resumption controls, it is - // possible that another notification has already been reused for the resumption - // controls of this package. In this case, rather than renaming this player as - // packageName, just remove it and then send a update to the existing resumption - // controls. - notifyMediaDataRemoved(key) - notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated) - } - logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId) + fun dismissSmartspaceRecommendation(key: String, delay: Long) - // Limit total number of resume controls - val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption } - val numResume = resumeEntries.size - if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { - resumeEntries - .toList() - .sortedBy { (key, data) -> data.lastActive } - .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) - .forEach { (key, data) -> - Log.d(TAG, "Removing excess control $key") - mediaEntries.remove(key) - notifyMediaDataRemoved(key) - logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) - } - } - } - - fun setMediaResumptionEnabled(isEnabled: Boolean) { - if (useMediaResumption == isEnabled) { - return - } + /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ + fun setRecommendationInactive(key: String) - useMediaResumption = isEnabled + /** Invoked when notification is removed. */ + fun onNotificationRemoved(key: String) - if (!useMediaResumption) { - // Remove any existing resume controls - val filtered = mediaEntries.filter { !it.value.active } - filtered.forEach { - mediaEntries.remove(it.key) - notifyMediaDataRemoved(it.key) - logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId) - } - } - } + fun setMediaResumptionEnabled(isEnabled: Boolean) /** Invoked when the user has dismissed the media carousel */ - fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss() + fun onSwipeToDismiss() /** Are there any media notifications active, including the recommendations? */ - fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation() + fun hasActiveMediaOrRecommendation(): Boolean - /** - * Are there any media entries we should display, including the recommendations? - * - If resumption is enabled, this will include inactive players - * - If resumption is disabled, we only want to show active players - */ - fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation() + /** Are there any media entries we should display, including the recommendations? */ + fun hasAnyMediaOrRecommendation(): Boolean /** Are there any resume media notifications active, excluding the recommendations? */ - fun hasActiveMedia() = mediaDataFilter.hasActiveMedia() + fun hasActiveMedia(): Boolean - /** - * Are there any resume media notifications active, excluding the recommendations? - * - If resumption is enabled, this will include inactive players - * - If resumption is disabled, we only want to show active players - */ - fun hasAnyMedia() = mediaDataFilter.hasAnyMedia() + /** Are there any resume media notifications active, excluding the recommendations? */ + fun hasAnyMedia(): Boolean + + /** Is recommendation card active? */ + fun isRecommendationActive(): Boolean - interface Listener { + // Uses [MediaDataProcessor.Listener] in order to link the new logic code with UI layer. + interface Listener : MediaDataProcessor.Listener { /** * Called whenever there's new MediaData Loaded for the consumption in views. @@ -1637,13 +113,13 @@ class MediaDataManager( * @param isSsReactivated indicates resume media card is reactivated by Smartspace * recommendation signal */ - fun onMediaDataLoaded( + override fun onMediaDataLoaded( key: String, oldKey: String?, data: MediaData, - immediately: Boolean = true, - receivedSmartspaceCardLatency: Int = 0, - isSsReactivated: Boolean = false + immediately: Boolean, + receivedSmartspaceCardLatency: Int, + isSsReactivated: Boolean, ) {} /** @@ -1653,14 +129,14 @@ class MediaDataManager( * it will be prioritized as the first card. Otherwise, it will show up as the last card * as default. */ - fun onSmartspaceMediaDataLoaded( + override fun onSmartspaceMediaDataLoaded( key: String, data: SmartspaceMediaData, - shouldPrioritize: Boolean = false + shouldPrioritize: Boolean, ) {} /** Called whenever a previously existing Media notification was removed. */ - fun onMediaDataRemoved(key: String) {} + override fun onMediaDataRemoved(key: String) {} /** * Called whenever a previously existing Smartspace media data was removed. @@ -1669,78 +145,14 @@ class MediaDataManager( * until the next refresh-round before UI becomes visible. True by default to take in * place immediately. */ - fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {} + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {} } - /** - * Converts the pass-in SmartspaceTarget to SmartspaceMediaData - * - * @return An empty SmartspaceMediaData with the valid target Id is returned if the - * SmartspaceTarget's data is invalid. - */ - private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData { - val baseAction: SmartspaceAction? = target.baseAction - val dismissIntent = - baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? - - val isActive = - when { - !mediaFlags.isPersistentSsCardEnabled() -> true - baseAction == null -> true - else -> { - val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE) - triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC - } - } - - packageName(target)?.let { - return SmartspaceMediaData( - targetId = target.smartspaceTargetId, - isActive = isActive, - packageName = it, - cardAction = target.baseAction, - recommendations = target.iconGrid, - dismissIntent = dismissIntent, - headphoneConnectionTimeMillis = target.creationTimeMillis, - instanceId = logger.getNewInstanceId(), - expiryTimeMs = target.expiryTimeMillis, - ) - } - return EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = target.smartspaceTargetId, - isActive = isActive, - dismissIntent = dismissIntent, - headphoneConnectionTimeMillis = target.creationTimeMillis, - instanceId = logger.getNewInstanceId(), - expiryTimeMs = target.expiryTimeMillis, - ) - } - - private fun packageName(target: SmartspaceTarget): String? { - val recommendationList = target.iconGrid - if (recommendationList == null || recommendationList.isEmpty()) { - Log.w(TAG, "Empty or null media recommendation list.") - return null - } - for (recommendation in recommendationList) { - val extras = recommendation.extras - extras?.let { - it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName -> - return packageName - } - } - } - Log.w(TAG, "No valid package name is provided.") - return null - } + companion object { - override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.apply { - println("internalListeners: $internalListeners") - println("externalListeners: ${mediaDataFilter.listeners}") - println("mediaEntries: $mediaEntries") - println("useMediaResumption: $useMediaResumption") - println("allowMediaRecommendations: $allowMediaRecommendations") + @JvmStatic + fun isMediaNotification(sbn: StatusBarNotification): Boolean { + return sbn.notification.isMediaNotification() } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt new file mode 100644 index 000000000000..7412290e8fc5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -0,0 +1,1654 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.domain.pipeline + +import android.annotation.SuppressLint +import android.app.ActivityOptions +import android.app.BroadcastOptions +import android.app.Notification +import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME +import android.app.PendingIntent +import android.app.StatusBarManager +import android.app.UriGrantsManager +import android.app.smartspace.SmartspaceAction +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.app.smartspace.SmartspaceTarget +import android.content.BroadcastReceiver +import android.content.ContentProvider +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.graphics.drawable.Animatable +import android.graphics.drawable.Icon +import android.media.MediaDescription +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.net.Uri +import android.os.Handler +import android.os.Parcelable +import android.os.Process +import android.os.UserHandle +import android.provider.Settings +import android.service.notification.StatusBarNotification +import android.support.v4.media.MediaMetadataCompat +import android.text.TextUtils +import android.util.Log +import android.util.Pair as APair +import androidx.media.utils.MediaConstants +import com.android.app.tracing.traceSection +import com.android.internal.annotations.Keep +import com.android.internal.logging.InstanceId +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.CoreStartable +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.data.repository.MediaDataRepository +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser +import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE +import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC +import com.android.systemui.media.controls.shared.model.MediaAction +import com.android.systemui.media.controls.shared.model.MediaButton +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDeviceData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.ui.view.MediaViewHolder +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaDataUtils +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.res.R +import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState +import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState +import com.android.systemui.statusbar.notification.row.HybridGroupManager +import com.android.systemui.util.Assert +import com.android.systemui.util.Utils +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.concurrency.ThreadFactory +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import com.android.systemui.util.time.SystemClock +import java.io.IOException +import java.io.PrintWriter +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +// URI fields to try loading album art from +private val ART_URIS = + arrayOf( + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI + ) + +private const val TAG = "MediaDataProcessor" +private const val DEBUG = true +private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent" + +/** Processes all media data fields and encapsulates logic for managing media data entries. */ +@SysUISingleton +class MediaDataProcessor( + private val context: Context, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundExecutor: Executor, + @Main private val uiExecutor: Executor, + @Main private val foregroundExecutor: DelayableExecutor, + @Main private val handler: Handler, + private val mediaControllerFactory: MediaControllerFactory, + private val broadcastDispatcher: BroadcastDispatcher, + private val dumpManager: DumpManager, + private val activityStarter: ActivityStarter, + private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider, + private var useMediaResumption: Boolean, + private val useQsMediaPlayer: Boolean, + private val systemClock: SystemClock, + private val secureSettings: SecureSettings, + private val mediaFlags: MediaFlags, + private val logger: MediaUiEventLogger, + private val smartspaceManager: SmartspaceManager?, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val mediaDataRepository: MediaDataRepository, +) : CoreStartable, BcSmartspaceDataPlugin.SmartspaceTargetListener { + + companion object { + /** + * UI surface label for subscribing Smartspace updates. String must match with + * [BcSmartspaceDataPlugin.UI_SURFACE_MEDIA] + */ + @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" + + // Smartspace package name's extra key. + @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" + + // Maximum number of actions allowed in compact view + @JvmField val MAX_COMPACT_ACTIONS = 3 + + /** + * Maximum number of actions allowed in expanded view. Number must match with the size of + * [MediaViewHolder.genericButtonIds] + */ + @JvmField val MAX_NOTIFICATION_ACTIONS = 5 + } + + private val themeText = + com.android.settingslib.Utils.getColorAttr( + context, + com.android.internal.R.attr.textColorPrimary + ) + .defaultColor + + // Internal listeners are part of the internal pipeline. External listeners (those registered + // with [MediaDeviceManager.addListener]) receive events after they have propagated through + // the internal pipeline. + // Another way to think of the distinction between internal and external listeners is the + // following. Internal listeners are listeners that MediaDataProcessor depends on, and external + // listeners are listeners that depend on MediaDataProcessor. + private val internalListeners: MutableSet<Listener> = mutableSetOf() + + // There should ONLY be at most one Smartspace media recommendation. + @Keep private var smartspaceSession: SmartspaceSession? = null + private var allowMediaRecommendations = false + + private val artworkWidth = + context.resources.getDimensionPixelSize( + com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize + ) + private val artworkHeight = + context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) + + @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE + private val statusBarManager = + context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager + + /** Check whether this notification is an RCN */ + private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean { + return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) + } + + @Inject + constructor( + context: Context, + @Application applicationScope: CoroutineScope, + @Background backgroundDispatcher: CoroutineDispatcher, + threadFactory: ThreadFactory, + @Main uiExecutor: Executor, + @Main foregroundExecutor: DelayableExecutor, + @Main handler: Handler, + mediaControllerFactory: MediaControllerFactory, + dumpManager: DumpManager, + broadcastDispatcher: BroadcastDispatcher, + activityStarter: ActivityStarter, + smartspaceMediaDataProvider: SmartspaceMediaDataProvider, + clock: SystemClock, + secureSettings: SecureSettings, + mediaFlags: MediaFlags, + logger: MediaUiEventLogger, + smartspaceManager: SmartspaceManager?, + keyguardUpdateMonitor: KeyguardUpdateMonitor, + mediaDataRepository: MediaDataRepository, + ) : this( + context, + applicationScope, + backgroundDispatcher, + // Loading bitmap for UMO background can take longer time, so it cannot run on the default + // background thread. Use a custom thread for media. + threadFactory.buildExecutorOnNewThread(TAG), + uiExecutor, + foregroundExecutor, + handler, + mediaControllerFactory, + broadcastDispatcher, + dumpManager, + activityStarter, + smartspaceMediaDataProvider, + Utils.useMediaResumption(context), + Utils.useQsMediaPlayer(context), + clock, + secureSettings, + mediaFlags, + logger, + smartspaceManager, + keyguardUpdateMonitor, + mediaDataRepository, + ) + + private val appChangeReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + Intent.ACTION_PACKAGES_SUSPENDED -> { + val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) + packages?.forEach { removeAllForPackage(it) } + } + Intent.ACTION_PACKAGE_REMOVED, + Intent.ACTION_PACKAGE_RESTARTED -> { + intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) } + } + } + } + } + + override fun start() { + if (!mediaFlags.isMediaControlsRefactorEnabled()) { + return + } + + dumpManager.registerNormalDumpable(TAG, this) + + val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) + broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) + + val uninstallFilter = + IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_REMOVED) + addAction(Intent.ACTION_PACKAGE_RESTARTED) + addDataScheme("package") + } + // BroadcastDispatcher does not allow filters with data schemes + context.registerReceiver(appChangeReceiver, uninstallFilter) + + // Register for Smartspace data updates. + smartspaceMediaDataProvider.registerListener(this) + smartspaceSession = + smartspaceManager?.createSmartspaceSession( + SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() + ) + smartspaceSession?.let { + it.addOnTargetsAvailableListener( + // Use a main uiExecutor thread listening to Smartspace updates instead of using + // the existing background executor. + // SmartspaceSession has scheduled routine updates which can be unpredictable on + // test simulators, using the backgroundExecutor makes it's hard to test the threads + // numbers. + uiExecutor + ) { targets -> + smartspaceMediaDataProvider.onTargetsAvailable(targets) + } + } + smartspaceSession?.requestSmartspaceUpdate() + + // Track media controls recommendation setting. + applicationScope.launch { trackMediaControlsRecommendationSetting() } + } + + fun destroy() { + smartspaceMediaDataProvider.unregisterListener(this) + smartspaceSession?.close() + smartspaceSession = null + context.unregisterReceiver(appChangeReceiver) + internalListeners.clear() + } + + fun onNotificationAdded(key: String, sbn: StatusBarNotification) { + if (useQsMediaPlayer && isMediaNotification(sbn)) { + var isNewlyActiveEntry = false + Assert.isMainThread() + val oldKey = findExistingEntry(key, sbn.packageName) + if (oldKey == null) { + val instanceId = logger.getNewInstanceId() + val temp = + MediaData() + .copy( + packageName = sbn.packageName, + instanceId = instanceId, + createdTimestampMillis = systemClock.currentTimeMillis(), + ) + mediaDataRepository.addMediaEntry(key, temp) + isNewlyActiveEntry = true + } else if (oldKey != key) { + // Resume -> active conversion; move to new key + val oldData = mediaDataRepository.removeMediaEntry(oldKey)!! + isNewlyActiveEntry = true + mediaDataRepository.addMediaEntry(key, oldData) + } + loadMediaData(key, sbn, oldKey, isNewlyActiveEntry) + } else { + onNotificationRemoved(key) + } + } + + /** + * Allow recommendations from smartspace to show in media controls. Requires + * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0 + */ + private suspend fun allowMediaRecommendations(): Boolean { + return withContext(backgroundDispatcher) { + val flag = + secureSettings.getBoolForUser( + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + true, + UserHandle.USER_CURRENT + ) + + useQsMediaPlayer && flag + } + } + + private suspend fun trackMediaControlsRecommendationSetting() { + secureSettings + .observerFlow(UserHandle.USER_ALL, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION) + // perform a query at the beginning. + .onStart { emit(Unit) } + .map { allowMediaRecommendations() } + .distinctUntilChanged() + // only track the most recent emission + .collectLatest { + allowMediaRecommendations = it + if (!allowMediaRecommendations) { + dismissSmartspaceRecommendation( + key = mediaDataRepository.smartspaceMediaData.value.targetId, + delay = 0L + ) + } + } + } + + private fun removeAllForPackage(packageName: String) { + Assert.isMainThread() + val toRemove = + mediaDataRepository.mediaEntries.value.filter { it.value.packageName == packageName } + toRemove.forEach { removeEntry(it.key) } + } + + fun setResumeAction(key: String, action: Runnable?) { + mediaDataRepository.mediaEntries.value.get(key)?.let { + it.resumeAction = action + it.hasCheckedForResume = true + } + } + + fun addResumptionControls( + userId: Int, + desc: MediaDescription, + action: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + // Resume controls don't have a notification key, so store by package name instead + if (!mediaDataRepository.mediaEntries.value.containsKey(packageName)) { + val instanceId = logger.getNewInstanceId() + val appUid = + try { + context.packageManager.getApplicationInfo(packageName, 0).uid + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app UID for $packageName", e) + Process.INVALID_UID + } + + val resumeData = + MediaData() + .copy( + packageName = packageName, + resumeAction = action, + hasCheckedForResume = true, + instanceId = instanceId, + appUid = appUid, + createdTimestampMillis = systemClock.currentTimeMillis(), + ) + mediaDataRepository.addMediaEntry(packageName, resumeData) + logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) + logger.logResumeMediaAdded(appUid, packageName, instanceId) + } + backgroundExecutor.execute { + loadMediaDataInBgForResumption( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) + } + } + + /** + * Check if there is an existing entry that matches the key or package name. Returns the key + * that matches, or null if not found. + */ + private fun findExistingEntry(key: String, packageName: String): String? { + val mediaEntries = mediaDataRepository.mediaEntries.value + if (mediaEntries.containsKey(key)) { + return key + } + // Check if we already had a resume player + if (mediaEntries.containsKey(packageName)) { + return packageName + } + return null + } + + private fun loadMediaData( + key: String, + sbn: StatusBarNotification, + oldKey: String?, + isNewlyActiveEntry: Boolean = false, + ) { + backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } + } + + /** Add a listener for internal events. */ + fun addInternalListener(listener: Listener) = internalListeners.add(listener) + + /** + * Notify internal listeners of media loaded event. + * + * External listeners registered with [MediaCarouselInteractor.addListener] will be notified + * after the event propagates through the internal listener pipeline. + */ + private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { + internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) } + } + + /** + * Notify internal listeners of Smartspace media loaded event. + * + * External listeners registered with [MediaCarouselInteractor.addListener] will be notified + * after the event propagates through the internal listener pipeline. + */ + private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { + internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) } + } + + /** + * Notify internal listeners of media removed event. + * + * External listeners registered with [MediaCarouselInteractor.addListener] will be notified + * after the event propagates through the internal listener pipeline. + */ + private fun notifyMediaDataRemoved(key: String) { + internalListeners.forEach { it.onMediaDataRemoved(key) } + } + + /** + * Notify internal listeners of Smartspace media removed event. + * + * External listeners registered with [MediaCarouselInteractor.addListener] will be notified + * after the event propagates through the internal listener pipeline. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait until + * the next refresh-round before UI becomes visible. Should only be true if the update is + * initiated by user's interaction. + */ + private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } + } + + /** + * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This + * will make the player not active anymore, hiding it from QQS and Keyguard. + * + * @see MediaData.active + */ + fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean = false) { + mediaDataRepository.mediaEntries.value[key]?.let { + if (timedOut && !forceUpdate) { + // Only log this event when media expires on its own + logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId) + } + if (it.active == !timedOut && !forceUpdate) { + if (it.resumption) { + if (DEBUG) Log.d(TAG, "timing out resume player $key") + dismissMediaData(key, 0L /* delay */) + } + return + } + // Update last active if media was still active. + if (it.active) { + it.lastActive = systemClock.elapsedRealtime() + } + it.active = !timedOut + if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") + onMediaDataLoaded(key, key, it) + } + + if (key == mediaDataRepository.smartspaceMediaData.value.targetId) { + if (DEBUG) Log.d(TAG, "smartspace card expired") + dismissSmartspaceRecommendation(key, delay = 0L) + } + } + + /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ + internal fun updateState(key: String, state: PlaybackState) { + mediaDataRepository.mediaEntries.value.get(key)?.let { + val token = it.token + if (token == null) { + if (DEBUG) Log.d(TAG, "State updated, but token was null") + return + } + val actions = + createActionsFromState( + it.packageName, + mediaControllerFactory.create(it.token), + UserHandle(it.userId) + ) + + // Control buttons + // If flag is enabled and controller has a PlaybackState, + // create actions from session info + // otherwise, no need to update semantic actions. + val data = + if (actions != null) { + it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) + } else { + it.copy(isPlaying = isPlayingState(state.state)) + } + if (DEBUG) Log.d(TAG, "State updated outside of notification") + onMediaDataLoaded(key, key, data) + } + } + + private fun removeEntry(key: String, logEvent: Boolean = true) { + mediaDataRepository.removeMediaEntry(key)?.let { + if (logEvent) { + logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) + } + } + notifyMediaDataRemoved(key) + } + + /** Dismiss a media entry. Returns false if the key was not found. */ + fun dismissMediaData(key: String, delay: Long): Boolean { + val existed = mediaDataRepository.mediaEntries.value[key] != null + backgroundExecutor.execute { + mediaDataRepository.mediaEntries.value[key]?.let { mediaData -> + if (mediaData.isLocalSession()) { + mediaData.token?.let { + val mediaController = mediaControllerFactory.create(it) + mediaController.transportControls.stop() + } + } + } + } + foregroundExecutor.executeDelayed({ removeEntry(key) }, delay) + return existed + } + + /** + * Called whenever the recommendation has been expired or removed by the user. This will remove + * the recommendation card entirely from the carousel. + */ + fun dismissSmartspaceRecommendation(key: String, delay: Long) { + if (mediaDataRepository.dismissSmartspaceRecommendation(key)) { + foregroundExecutor.executeDelayed( + { notifySmartspaceMediaDataRemoved(key, immediately = true) }, + delay + ) + } + } + + /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ + fun setRecommendationInactive(key: String) { + if (mediaDataRepository.setRecommendationInactive(key)) { + val recommendation = mediaDataRepository.smartspaceMediaData.value + notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation) + } + } + + private fun loadMediaDataInBgForResumption( + userId: Int, + desc: MediaDescription, + resumeAction: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + if (desc.title.isNullOrBlank()) { + Log.e(TAG, "Description incomplete") + // Delete the placeholder entry + mediaDataRepository.removeMediaEntry(packageName) + return + } + + if (DEBUG) { + Log.d(TAG, "adding track for $userId from browser: $desc") + } + + val currentEntry = mediaDataRepository.mediaEntries.value.get(packageName) + val appUid = currentEntry?.appUid ?: Process.INVALID_UID + + // Album art + var artworkBitmap = desc.iconBitmap + if (artworkBitmap == null && desc.iconUri != null) { + artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName) + } + val artworkIcon = + if (artworkBitmap != null) { + Icon.createWithBitmap(artworkBitmap) + } else { + null + } + + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val isExplicit = + desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + + val progress = + if (mediaFlags.isResumeProgressEnabled()) { + MediaDataUtils.getDescriptionProgress(desc.extras) + } else null + + val mediaAction = getResumeMediaAction(resumeAction) + val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + foregroundExecutor.execute { + onMediaDataLoaded( + packageName, + null, + MediaData( + userId, + true, + appName, + null, + desc.subtitle, + desc.title, + artworkIcon, + listOf(mediaAction), + listOf(0), + MediaButton(playOrPause = mediaAction), + packageName, + token, + appIntent, + device = null, + active = false, + resumeAction = resumeAction, + resumption = true, + notificationKey = packageName, + hasCheckedForResume = true, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = appUid, + isExplicit = isExplicit, + resumeProgress = progress, + ) + ) + } + } + + fun loadMediaDataInBg( + key: String, + sbn: StatusBarNotification, + oldKey: String?, + isNewlyActiveEntry: Boolean = false, + ) { + val token = + sbn.notification.extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION, + MediaSession.Token::class.java + ) + if (token == null) { + return + } + val mediaController = mediaControllerFactory.create(token) + val metadata = mediaController.metadata + val notif: Notification = sbn.notification + + val appInfo = + notif.extras.getParcelable( + Notification.EXTRA_BUILDER_APPLICATION_INFO, + ApplicationInfo::class.java + ) + ?: getAppInfoFromPackage(sbn.packageName) + + // App name + val appName = getAppName(sbn, appInfo) + + // Song name + var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (song.isNullOrBlank()) { + song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) + } + if (song.isNullOrBlank()) { + song = HybridGroupManager.resolveTitle(notif) + } + if (song.isNullOrBlank()) { + // For apps that don't include a title, log and add a placeholder + song = context.getString(R.string.controls_media_empty_title, appName) + try { + statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier) + } catch (e: RuntimeException) { + Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}") + } + } + + // Album art + var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } + if (artworkBitmap == null) { + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) + } + if (artworkBitmap == null) { + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + } + val artWorkIcon = + if (artworkBitmap == null) { + notif.getLargeIcon() + } else { + Icon.createWithBitmap(artworkBitmap) + } + + // App Icon + val smallIcon = sbn.notification.smallIcon + + // Explicit Indicator + val isExplicit: Boolean + val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) + isExplicit = + mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + + // Artist name + var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) + if (artist.isNullOrBlank()) { + artist = HybridGroupManager.resolveText(notif) + } + + // Device name (used for remote cast notifications) + var device: MediaDeviceData? = null + if (isRemoteCastNotification(sbn)) { + val extras = sbn.notification.extras + val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) + val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) + val deviceIntent = + extras.getParcelable( + Notification.EXTRA_MEDIA_REMOTE_INTENT, + PendingIntent::class.java + ) + Log.d(TAG, "$key is RCN for $deviceName") + + if (deviceName != null && deviceIcon > -1) { + // Name and icon must be present, but intent may be null + val enabled = deviceIntent != null && deviceIntent.isActivity + val deviceDrawable = + Icon.createWithResource(sbn.packageName, deviceIcon) + .loadDrawable(sbn.getPackageContext(context)) + device = + MediaDeviceData( + enabled, + deviceDrawable, + deviceName, + deviceIntent, + showBroadcastButton = false + ) + } + } + + // Control buttons + // If flag is enabled and controller has a PlaybackState, create actions from session info + // Otherwise, use the notification actions + var actionIcons: List<MediaAction> = emptyList() + var actionsToShowCollapsed: List<Int> = emptyList() + val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) + if (semanticActions == null) { + val actions = createActionsFromNotification(sbn) + actionIcons = actions.first + actionsToShowCollapsed = actions.second + } + + val playbackLocation = + if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE + else if ( + mediaController.playbackInfo?.playbackType == + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL + ) + MediaData.PLAYBACK_LOCAL + else MediaData.PLAYBACK_CAST_LOCAL + val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } + + val currentEntry = mediaDataRepository.mediaEntries.value.get(key) + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val appUid = appInfo?.uid ?: Process.INVALID_UID + + if (isNewlyActiveEntry) { + logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) + logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) + } else if (playbackLocation != currentEntry?.playbackLocation) { + logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation) + } + + val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + foregroundExecutor.execute { + val resumeAction: Runnable? = mediaDataRepository.mediaEntries.value[key]?.resumeAction + val hasCheckedForResume = + mediaDataRepository.mediaEntries.value[key]?.hasCheckedForResume == true + val active = mediaDataRepository.mediaEntries.value[key]?.active ?: true + onMediaDataLoaded( + key, + oldKey, + MediaData( + sbn.normalizedUserId, + true, + appName, + smallIcon, + artist, + song, + artWorkIcon, + actionIcons, + actionsToShowCollapsed, + semanticActions, + sbn.packageName, + token, + notif.contentIntent, + device, + active, + resumeAction = resumeAction, + playbackLocation = playbackLocation, + notificationKey = key, + hasCheckedForResume = hasCheckedForResume, + isPlaying = isPlaying, + isClearable = !sbn.isOngoing, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = appUid, + isExplicit = isExplicit, + ) + ) + } + } + + private fun logSingleVsMultipleMediaAdded( + appUid: Int, + packageName: String, + instanceId: InstanceId + ) { + if (mediaDataRepository.mediaEntries.value.size == 1) { + logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) + } else if (mediaDataRepository.mediaEntries.value.size == 2) { + // Since this method is only called when there is a new media session added. + // logging needed once there is more than one media session in carousel. + logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId) + } + } + + private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { + try { + return context.packageManager.getApplicationInfo(packageName, 0) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app info for $packageName", e) + } + return null + } + + private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { + val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) + if (name != null) { + return name + } + + return if (appInfo != null) { + context.packageManager.getApplicationLabel(appInfo).toString() + } else { + sbn.packageName + } + } + + /** Generate action buttons based on notification actions */ + private fun createActionsFromNotification( + sbn: StatusBarNotification + ): Pair<List<MediaAction>, List<Int>> { + val notif = sbn.notification + val actionIcons: MutableList<MediaAction> = ArrayList() + val actions = notif.actions + var actionsToShowCollapsed = + notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() + ?: mutableListOf() + if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { + Log.e( + TAG, + "Too many compact actions for ${sbn.key}," + + "limiting to first $MAX_COMPACT_ACTIONS" + ) + actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) + } + + if (actions != null) { + for ((index, action) in actions.withIndex()) { + if (index == MAX_NOTIFICATION_ACTIONS) { + Log.w( + TAG, + "Too many notification actions for ${sbn.key}," + + " limiting to first $MAX_NOTIFICATION_ACTIONS" + ) + break + } + if (action.getIcon() == null) { + if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}") + actionsToShowCollapsed.remove(index) + continue + } + val runnable = + if (action.actionIntent != null) { + Runnable { + if (action.actionIntent.isActivity) { + activityStarter.startPendingIntentDismissingKeyguard( + action.actionIntent + ) + } else if (action.isAuthenticationRequired()) { + activityStarter.dismissKeyguardThenExecute( + { + var result = sendPendingIntent(action.actionIntent) + result + }, + {}, + true + ) + } else { + sendPendingIntent(action.actionIntent) + } + } + } else { + null + } + val mediaActionIcon = + if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { + Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) + } else { + action.getIcon() + } + .setTint(themeText) + .loadDrawable(context) + val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) + actionIcons.add(mediaAction) + } + } + return Pair(actionIcons, actionsToShowCollapsed) + } + + /** + * Generates action button info for this media session based on the PlaybackState + * + * @param packageName Package name for the media app + * @param controller MediaController for the current session + * @return a Pair consisting of a list of media actions, and a list of ints representing which + * + * ``` + * of those actions should be shown in the compact player + * ``` + */ + private fun createActionsFromState( + packageName: String, + controller: MediaController, + user: UserHandle + ): MediaButton? { + val state = controller.playbackState + if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { + return null + } + + // First, check for standard actions + val playOrPause = + if (isConnectingState(state.state)) { + // Spinner needs to be animating to render anything. Start it here. + val drawable = + context.getDrawable(com.android.internal.R.drawable.progress_small_material) + (drawable as Animatable).start() + MediaAction( + drawable, + null, // no action to perform when clicked + context.getString(R.string.controls_media_button_connecting), + context.getDrawable(R.drawable.ic_media_connecting_container), + // Specify a rebind id to prevent the spinner from restarting on later binds. + com.android.internal.R.drawable.progress_small_material + ) + } else if (isPlayingState(state.state)) { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) + } else { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) + } + val prevButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) + val nextButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) + + // Then, create a way to build any custom actions that will be needed + val customActions = + state.customActions + .asSequence() + .filterNotNull() + .map { getCustomAction(packageName, controller, it) } + .iterator() + fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null + + // Finally, assign the remaining button slots: play/pause A B C D + // A = previous, else custom action (if not reserved) + // B = next, else custom action (if not reserved) + // C and D are always custom actions + val reservePrev = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV + ) == true + val reserveNext = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT + ) == true + + val prevOrCustom = + if (prevButton != null) { + prevButton + } else if (!reservePrev) { + nextCustomAction() + } else { + null + } + + val nextOrCustom = + if (nextButton != null) { + nextButton + } else if (!reserveNext) { + nextCustomAction() + } else { + null + } + + return MediaButton( + playOrPause, + nextOrCustom, + prevOrCustom, + nextCustomAction(), + nextCustomAction(), + reserveNext, + reservePrev + ) + } + + /** + * Create a [MediaAction] for a given action and media session + * + * @param controller MediaController for the session + * @param stateActions The actions included with the session's [PlaybackState] + * @param action A [PlaybackState.Actions] value representing what action to generate. One of: + * ``` + * [PlaybackState.ACTION_PLAY] + * [PlaybackState.ACTION_PAUSE] + * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] + * [PlaybackState.ACTION_SKIP_TO_NEXT] + * @return + * ``` + * + * A [MediaAction] with correct values set, or null if the state doesn't support it + */ + private fun getStandardAction( + controller: MediaController, + stateActions: Long, + @PlaybackState.Actions action: Long + ): MediaAction? { + if (!includesAction(stateActions, action)) { + return null + } + + return when (action) { + PlaybackState.ACTION_PLAY -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_play), + { controller.transportControls.play() }, + context.getString(R.string.controls_media_button_play), + context.getDrawable(R.drawable.ic_media_play_container) + ) + } + PlaybackState.ACTION_PAUSE -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_pause), + { controller.transportControls.pause() }, + context.getString(R.string.controls_media_button_pause), + context.getDrawable(R.drawable.ic_media_pause_container) + ) + } + PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_prev), + { controller.transportControls.skipToPrevious() }, + context.getString(R.string.controls_media_button_prev), + null + ) + } + PlaybackState.ACTION_SKIP_TO_NEXT -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_next), + { controller.transportControls.skipToNext() }, + context.getString(R.string.controls_media_button_next), + null + ) + } + else -> null + } + } + + /** Check whether the actions from a [PlaybackState] include a specific action */ + private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { + if ( + (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && + (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) + ) { + return true + } + return (stateActions and action != 0L) + } + + /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ + private fun getCustomAction( + packageName: String, + controller: MediaController, + customAction: PlaybackState.CustomAction + ): MediaAction { + return MediaAction( + Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), + { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, + customAction.name, + null + ) + } + + /** Load a bitmap from the various Art metadata URIs */ + private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { + for (uri in ART_URIS) { + val uriString = metadata.getString(uri) + if (!TextUtils.isEmpty(uriString)) { + val albumArt = loadBitmapFromUri(Uri.parse(uriString)) + if (albumArt != null) { + if (DEBUG) Log.d(TAG, "loaded art from $uri") + return albumArt + } + } + } + return null + } + + private fun sendPendingIntent(intent: PendingIntent): Boolean { + return try { + val options = BroadcastOptions.makeBasic() + options.setInteractive(true) + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + intent.send(options.toBundle()) + true + } catch (e: PendingIntent.CanceledException) { + Log.d(TAG, "Intent canceled", e) + false + } + } + + /** Returns a bitmap if the user can access the given URI, else null */ + private fun loadBitmapFromUriForUser( + uri: Uri, + userId: Int, + appUid: Int, + packageName: String, + ): Bitmap? { + try { + val ugm = UriGrantsManager.getService() + ugm.checkGrantUriPermission_ignoreNonSystem( + appUid, + packageName, + ContentProvider.getUriWithoutUserId(uri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(uri, userId) + ) + return loadBitmapFromUri(uri) + } catch (e: SecurityException) { + Log.e(TAG, "Failed to get URI permission: $e") + } + return null + } + + /** + * Load a bitmap from a URI + * + * @param uri the uri to load + * @return bitmap, or null if couldn't be loaded + */ + private fun loadBitmapFromUri(uri: Uri): Bitmap? { + // ImageDecoder requires a scheme of the following types + if (uri.scheme == null) { + return null + } + + if ( + !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && + !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) && + !uri.scheme.equals(ContentResolver.SCHEME_FILE) + ) { + return null + } + + val source = ImageDecoder.createSource(context.contentResolver, uri) + return try { + ImageDecoder.decodeBitmap(source) { decoder, info, _ -> + val width = info.size.width + val height = info.size.height + val scale = + MediaDataUtils.getScaleFactor( + APair(width, height), + APair(artworkWidth, artworkHeight) + ) + + // Downscale if needed + if (scale != 0f && scale < 1) { + decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt()) + } + decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE + } + } catch (e: IOException) { + Log.e(TAG, "Unable to load bitmap", e) + null + } catch (e: RuntimeException) { + Log.e(TAG, "Unable to load bitmap", e) + null + } + } + + private fun getResumeMediaAction(action: Runnable): MediaAction { + return MediaAction( + Icon.createWithResource(context, R.drawable.ic_media_play) + .setTint(themeText) + .loadDrawable(context), + action, + context.getString(R.string.controls_media_resume), + context.getDrawable(R.drawable.ic_media_play_container) + ) + } + + fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = + traceSection("MediaDataProcessor#onMediaDataLoaded") { + Assert.isMainThread() + if (mediaDataRepository.mediaEntries.value.containsKey(key)) { + // Otherwise this was removed already + mediaDataRepository.addMediaEntry(key, data) + notifyMediaDataLoaded(key, oldKey, data) + } + } + + override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { + if (!allowMediaRecommendations) { + if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.") + return + } + + val mediaTargets = targets.filterIsInstance<SmartspaceTarget>() + val smartspaceMediaData = mediaDataRepository.smartspaceMediaData.value + when (mediaTargets.size) { + 0 -> { + if (!smartspaceMediaData.isActive) { + return + } + if (DEBUG) { + Log.d(TAG, "Set Smartspace media to be inactive for the data update") + } + if (mediaFlags.isPersistentSsCardEnabled()) { + // Smartspace uses this signal to hide the card (e.g. when it expires or user + // disconnects headphones), so treat as setting inactive when flag is on + val recommendation = smartspaceMediaData.copy(isActive = false) + mediaDataRepository.setRecommendation(recommendation) + notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation) + } else { + notifySmartspaceMediaDataRemoved( + smartspaceMediaData.targetId, + immediately = false + ) + mediaDataRepository.setRecommendation( + SmartspaceMediaData( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId, + ) + ) + } + } + 1 -> { + val newMediaTarget = mediaTargets.get(0) + if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) { + // The same Smartspace updates can be received. Skip the duplicate updates. + return + } + if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") + val recommendation = toSmartspaceMediaData(newMediaTarget) + mediaDataRepository.setRecommendation(recommendation) + notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation) + } + else -> { + // There should NOT be more than 1 Smartspace media update. When it happens, it + // indicates a bad state or an error. Reset the status accordingly. + Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") + notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false) + mediaDataRepository.setRecommendation(SmartspaceMediaData()) + } + } + } + + fun onNotificationRemoved(key: String) { + Assert.isMainThread() + val removed = mediaDataRepository.removeMediaEntry(key) ?: return + if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } else if (isAbleToResume(removed)) { + convertToResumePlayer(key, removed) + } else if (mediaFlags.isRetainingPlayersEnabled()) { + handlePossibleRemoval(key, removed, notificationRemoved = true) + } else { + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } + } + + internal fun onSessionDestroyed(key: String) { + if (DEBUG) Log.d(TAG, "session destroyed for $key") + val entry = mediaDataRepository.removeMediaEntry(key) ?: return + // Clear token since the session is no longer valid + val updated = entry.copy(token = null) + handlePossibleRemoval(key, updated) + } + + private fun isAbleToResume(data: MediaData): Boolean { + val isEligibleForResume = + data.isLocalSession() || + (mediaFlags.isRemoteResumeAllowed() && + data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) + return useMediaResumption && data.resumeAction != null && isEligibleForResume + } + + /** + * Convert to resume state if the player is no longer valid and active, then notify listeners + * that the data was updated. Does not convert to resume state if the player is still valid, or + * if it was removed before becoming inactive. (Assumes that [removed] was removed from + * [mediaDataRepository.mediaEntries] state before this function was called) + */ + private fun handlePossibleRemoval( + key: String, + removed: MediaData, + notificationRemoved: Boolean = false + ) { + val hasSession = removed.token != null + if (hasSession && removed.semanticActions != null) { + // The app was using session actions, and the session is still valid: keep player + if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key") + mediaDataRepository.addMediaEntry(key, removed) + notifyMediaDataLoaded(key, key, removed) + } else if (!notificationRemoved && removed.semanticActions == null) { + // The app was using notification actions, and notif wasn't removed yet: keep player + if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key") + mediaDataRepository.addMediaEntry(key, removed) + notifyMediaDataLoaded(key, key, removed) + } else if (removed.active && !isAbleToResume(removed)) { + // This player was still active - it didn't last long enough to time out, + // and its app doesn't normally support resume: remove + if (DEBUG) Log.d(TAG, "Removing still-active player $key") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) { + // Convert to resume + if (DEBUG) { + Log.d( + TAG, + "Notification ($notificationRemoved) and/or session " + + "($hasSession) gone for inactive player $key" + ) + } + convertToResumePlayer(key, removed) + } else { + // Retaining players flag is off and app doesn't support resume: remove player. + if (DEBUG) Log.d(TAG, "Removing player $key") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } + } + + /** Set the given [MediaData] as a resume state player and notify listeners */ + private fun convertToResumePlayer(key: String, data: MediaData) { + if (DEBUG) Log.d(TAG, "Converting $key to resume") + // Resumption controls must have a title. + if (data.song.isNullOrBlank()) { + Log.e(TAG, "Description incomplete") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) + return + } + // Move to resume key (aka package name) if that key doesn't already exist. + val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) } + val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList() + val launcherIntent = + context.packageManager.getLaunchIntentForPackage(data.packageName)?.let { + PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE) + } + val lastActive = + if (data.active) { + systemClock.elapsedRealtime() + } else { + data.lastActive + } + val updated = + data.copy( + token = null, + actions = actions, + semanticActions = MediaButton(playOrPause = resumeAction), + actionsToShowInCompact = listOf(0), + active = false, + resumption = true, + isPlaying = false, + isClearable = true, + clickIntent = launcherIntent, + lastActive = lastActive, + ) + val pkg = data.packageName + val migrate = mediaDataRepository.addMediaEntry(pkg, updated) == null + // Notify listeners of "new" controls when migrating or removed and update when not + Log.d(TAG, "migrating? $migrate from $key -> $pkg") + if (migrate) { + notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated) + } else { + // Since packageName is used for the key of the resumption controls, it is + // possible that another notification has already been reused for the resumption + // controls of this package. In this case, rather than renaming this player as + // packageName, just remove it and then send a update to the existing resumption + // controls. + notifyMediaDataRemoved(key) + notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated) + } + logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId) + + // Limit total number of resume controls + val resumeEntries = + mediaDataRepository.mediaEntries.value.filter { (_, data) -> data.resumption } + val numResume = resumeEntries.size + if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { + resumeEntries + .toList() + .sortedBy { (_, data) -> data.lastActive } + .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) + .forEach { (key, data) -> + Log.d(TAG, "Removing excess control $key") + mediaDataRepository.removeMediaEntry(key) + notifyMediaDataRemoved(key) + logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) + } + } + } + + fun setMediaResumptionEnabled(isEnabled: Boolean) { + if (useMediaResumption == isEnabled) { + return + } + + useMediaResumption = isEnabled + + if (!useMediaResumption) { + // Remove any existing resume controls + val filtered = mediaDataRepository.mediaEntries.value.filter { !it.value.active } + filtered.forEach { + mediaDataRepository.removeMediaEntry(it.key) + notifyMediaDataRemoved(it.key) + logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId) + } + } + } + + /** Listener to data changes. */ + interface Listener { + + /** + * Called whenever there's new MediaData Loaded for the consumption in views. + * + * oldKey is provided to check whether the view has changed keys, which can happen when a + * player has gone from resume state (key is package name) to active state (key is + * notification key) or vice versa. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait + * until the next refresh-round before UI becomes visible. True by default to take in + * place immediately. + * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI + * displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace + * signal. + * @param isSsReactivated indicates resume media card is reactivated by Smartspace + * recommendation signal + */ + fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean = true, + receivedSmartspaceCardLatency: Int = 0, + isSsReactivated: Boolean = false + ) {} + + /** + * Called whenever there's new Smartspace media data loaded. + * + * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true, + * it will be prioritized as the first card. Otherwise, it will show up as the last card + * as default. + */ + fun onSmartspaceMediaDataLoaded( + key: String, + data: SmartspaceMediaData, + shouldPrioritize: Boolean = false + ) {} + + /** Called whenever a previously existing Media notification was removed. */ + fun onMediaDataRemoved(key: String) {} + + /** + * Called whenever a previously existing Smartspace media data was removed. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait + * until the next refresh-round before UI becomes visible. True by default to take in + * place immediately. + */ + fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {} + } + + /** + * Converts the pass-in SmartspaceTarget to SmartspaceMediaData + * + * @return An empty SmartspaceMediaData with the valid target Id is returned if the + * SmartspaceTarget's data is invalid. + */ + private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData { + val baseAction: SmartspaceAction? = target.baseAction + val dismissIntent = + baseAction + ?.extras + ?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY, Intent::class.java) + + val isActive = + when { + !mediaFlags.isPersistentSsCardEnabled() -> true + baseAction == null -> true + else -> { + val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE) + triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC + } + } + + packageName(target)?.let { + return SmartspaceMediaData( + targetId = target.smartspaceTargetId, + isActive = isActive, + packageName = it, + cardAction = target.baseAction, + recommendations = target.iconGrid, + dismissIntent = dismissIntent, + headphoneConnectionTimeMillis = target.creationTimeMillis, + instanceId = logger.getNewInstanceId(), + expiryTimeMs = target.expiryTimeMillis, + ) + } + return SmartspaceMediaData( + targetId = target.smartspaceTargetId, + isActive = isActive, + dismissIntent = dismissIntent, + headphoneConnectionTimeMillis = target.creationTimeMillis, + instanceId = logger.getNewInstanceId(), + expiryTimeMs = target.expiryTimeMillis, + ) + } + + private fun packageName(target: SmartspaceTarget): String? { + val recommendationList: MutableList<SmartspaceAction> = target.iconGrid + if (recommendationList.isEmpty()) { + Log.w(TAG, "Empty or null media recommendation list.") + return null + } + for (recommendation in recommendationList) { + val extras = recommendation.extras + extras?.let { + it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName -> + return packageName + } + } + } + Log.w(TAG, "No valid package name is provided.") + return null + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.apply { + println("internalListeners: $internalListeners") + println("useMediaResumption: $useMediaResumption") + println("allowMediaRecommendations: $allowMediaRecommendations") + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt index f4d70a5e78c9..c7cfb0b7d775 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt @@ -35,10 +35,8 @@ import com.android.settingslib.flags.Flags.legacyLeAudioSharing import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.settingslib.media.PhoneMediaDevice -import com.android.systemui.Dumpable import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDeviceData import com.android.systemui.media.controls.util.LocalMediaManagerFactory @@ -70,16 +68,11 @@ constructor( private val localBluetoothManager: Lazy<LocalBluetoothManager?>, @Main private val fgExecutor: Executor, @Background private val bgExecutor: Executor, - dumpManager: DumpManager, -) : MediaDataManager.Listener, Dumpable { +) : MediaDataManager.Listener { private val listeners: MutableSet<Listener> = mutableSetOf() private val entries: MutableMap<String, Entry> = mutableMapOf() - init { - dumpManager.registerDumpable(this) - } - /** Add a listener for changes to the media route (ie. device). */ fun addListener(listener: Listener) = listeners.add(listener) @@ -123,7 +116,7 @@ constructor( token?.let { listeners.forEach { it.onKeyRemoved(key) } } } - override fun dump(pw: PrintWriter, args: Array<String>) { + fun dump(pw: PrintWriter) { with(pw) { println("MediaDeviceManager state:") entries.forEach { (key, entry) -> diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt new file mode 100644 index 000000000000..4a92b71f1155 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.domain.pipeline.interactor + +import android.app.PendingIntent +import android.media.MediaDescription +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.service.notification.StatusBarNotification +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.media.controls.data.repository.MediaDataRepository +import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.MediaDataCombineLatest +import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager +import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor +import com.android.systemui.media.controls.domain.pipeline.MediaDeviceManager +import com.android.systemui.media.controls.domain.pipeline.MediaSessionBasedFilter +import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener +import com.android.systemui.media.controls.domain.resume.MediaResumeListener +import com.android.systemui.media.controls.util.MediaFlags +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** Encapsulates business logic for media pipeline. */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class MediaCarouselInteractor +@Inject +constructor( + @Application applicationScope: CoroutineScope, + private val mediaDataRepository: MediaDataRepository, + private val mediaDataProcessor: MediaDataProcessor, + private val mediaTimeoutListener: MediaTimeoutListener, + private val mediaResumeListener: MediaResumeListener, + private val mediaSessionBasedFilter: MediaSessionBasedFilter, + private val mediaDeviceManager: MediaDeviceManager, + private val mediaDataCombineLatest: MediaDataCombineLatest, + private val mediaDataFilter: MediaDataFilterImpl, + mediaFilterRepository: MediaFilterRepository, + private val mediaFlags: MediaFlags, +) : MediaDataManager, CoreStartable { + + /** Are there any media notifications active, including the recommendations? */ + val hasActiveMediaOrRecommendation: StateFlow<Boolean> = + combine( + mediaFilterRepository.selectedUserEntries, + mediaFilterRepository.smartspaceMediaData, + mediaFilterRepository.reactivatedKey + ) { entries, smartspaceMediaData, reactivatedKey -> + entries.any { it.value.active } || + (smartspaceMediaData.isActive && + (smartspaceMediaData.isValid() || reactivatedKey != null)) + } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + + /** Are there any media entries we should display, including the recommendations? */ + val hasAnyMediaOrRecommendation: StateFlow<Boolean> = + combine( + mediaFilterRepository.selectedUserEntries, + mediaFilterRepository.smartspaceMediaData + ) { entries, smartspaceMediaData -> + entries.isNotEmpty() || + (if (mediaFlags.isPersistentSsCardEnabled()) { + smartspaceMediaData.isValid() + } else { + smartspaceMediaData.isActive && smartspaceMediaData.isValid() + }) + } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + + /** Are there any media notifications active, excluding the recommendations? */ + val hasActiveMedia: StateFlow<Boolean> = + mediaFilterRepository.selectedUserEntries + .mapLatest { entries -> entries.any { it.value.active } } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + + /** Are there any media notifications, excluding the recommendations? */ + val hasAnyMedia: StateFlow<Boolean> = + mediaFilterRepository.selectedUserEntries + .mapLatest { entries -> entries.isNotEmpty() } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + + override fun start() { + if (!mediaFlags.isMediaControlsRefactorEnabled()) { + return + } + + // Initialize the internal processing pipeline. The listeners at the front of the pipeline + // are set as internal listeners so that they receive events. From there, events are + // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, + // so it is responsible for dispatching events to external listeners. To achieve this, + // external listeners that are registered with [MediaDataManager.addListener] are actually + // registered as listeners to mediaDataFilter. + addInternalListener(mediaTimeoutListener) + addInternalListener(mediaResumeListener) + addInternalListener(mediaSessionBasedFilter) + mediaSessionBasedFilter.addListener(mediaDeviceManager) + mediaSessionBasedFilter.addListener(mediaDataCombineLatest) + mediaDeviceManager.addListener(mediaDataCombineLatest) + mediaDataCombineLatest.addListener(mediaDataFilter) + + // Set up links back into the pipeline for listeners that need to send events upstream. + mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> + setInactive(key, timedOut) + } + mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> + mediaDataProcessor.updateState(key, state) + } + mediaTimeoutListener.sessionCallback = { key: String -> + mediaDataProcessor.onSessionDestroyed(key) + } + mediaResumeListener.setManager(this) + mediaDataFilter.mediaDataManager = this + } + + override fun addListener(listener: MediaDataManager.Listener) { + mediaDataFilter.addListener(listener) + } + + override fun removeListener(listener: MediaDataManager.Listener) { + mediaDataFilter.removeListener(listener) + } + + override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) { + mediaDataProcessor.setInactive(key, timedOut, forceUpdate) + } + + override fun onNotificationAdded(key: String, sbn: StatusBarNotification) { + mediaDataProcessor.onNotificationAdded(key, sbn) + } + + override fun destroy() { + mediaSessionBasedFilter.removeListener(mediaDeviceManager) + mediaSessionBasedFilter.removeListener(mediaDataCombineLatest) + mediaDeviceManager.removeListener(mediaDataCombineLatest) + mediaDataCombineLatest.removeListener(mediaDataFilter) + mediaDataProcessor.destroy() + } + + override fun setResumeAction(key: String, action: Runnable?) { + mediaDataProcessor.setResumeAction(key, action) + } + + override fun addResumptionControls( + userId: Int, + desc: MediaDescription, + action: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + mediaDataProcessor.addResumptionControls( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) + } + + override fun dismissMediaData(key: String, delay: Long): Boolean { + return mediaDataProcessor.dismissMediaData(key, delay) + } + + override fun dismissSmartspaceRecommendation(key: String, delay: Long) { + return mediaDataProcessor.dismissSmartspaceRecommendation(key, delay) + } + + override fun setRecommendationInactive(key: String) { + mediaDataProcessor.setRecommendationInactive(key) + } + + override fun onNotificationRemoved(key: String) { + mediaDataProcessor.onNotificationRemoved(key) + } + + override fun setMediaResumptionEnabled(isEnabled: Boolean) { + mediaDataProcessor.setMediaResumptionEnabled(isEnabled) + } + + override fun onSwipeToDismiss() { + mediaDataFilter.onSwipeToDismiss() + } + + override fun hasActiveMediaOrRecommendation() = hasActiveMediaOrRecommendation.value + + override fun hasAnyMediaOrRecommendation() = hasAnyMediaOrRecommendation.value + + override fun hasActiveMedia() = hasActiveMedia.value + + override fun hasAnyMedia() = hasAnyMedia.value + + override fun isRecommendationActive() = mediaDataRepository.smartspaceMediaData.value.isActive + + /** Add a listener for internal events. */ + private fun addInternalListener(listener: MediaDataManager.Listener) = + mediaDataProcessor.addInternalListener(listener) + + override fun dump(pw: PrintWriter, args: Array<out String>) { + mediaDeviceManager.dump(pw) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt index 4fa7cb54431f..11a562911a85 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt @@ -20,48 +20,49 @@ import android.app.PendingIntent import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.media.session.MediaSession +import android.os.Process import com.android.internal.logging.InstanceId import com.android.systemui.res.R /** State of a media view. */ data class MediaData( - val userId: Int, + val userId: Int = -1, val initialized: Boolean = false, /** App name that will be displayed on the player. */ - val app: String?, + val app: String? = null, /** App icon shown on player. */ - val appIcon: Icon?, + val appIcon: Icon? = null, /** Artist name. */ - val artist: CharSequence?, + val artist: CharSequence? = null, /** Song name. */ - val song: CharSequence?, + val song: CharSequence? = null, /** Album artwork. */ - val artwork: Icon?, + val artwork: Icon? = null, /** List of generic action buttons for the media player, based on notification actions */ - val actions: List<MediaAction>, + val actions: List<MediaAction> = emptyList(), /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */ - val actionsToShowInCompact: List<Int>, + val actionsToShowInCompact: List<Int> = emptyList(), /** * Semantic actions buttons, based on the PlaybackState of the media session. If present, these * actions will be preferred in the UI over [actions] */ val semanticActions: MediaButton? = null, /** Package name of the app that's posting the media. */ - val packageName: String, + val packageName: String = "INVALID", /** Unique media session identifier. */ - val token: MediaSession.Token?, + val token: MediaSession.Token? = null, /** Action to perform when the player is tapped. This is unrelated to {@link #actions}. */ - val clickIntent: PendingIntent?, + val clickIntent: PendingIntent? = null, /** Where the media is playing: phone, headphones, ear buds, remote session. */ - val device: MediaDeviceData?, + val device: MediaDeviceData? = null, /** * When active, a player will be displayed on keyguard and quick-quick settings. This is * unrelated to the stream being playing or not, a player will not be active if timed out, or in * resumption mode. */ - var active: Boolean, + var active: Boolean = true, /** Action that should be performed to restart a non active session. */ - var resumeAction: Runnable?, + var resumeAction: Runnable? = null, /** Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE */ var playbackLocation: Int = PLAYBACK_LOCAL, /** @@ -88,10 +89,10 @@ data class MediaData( var createdTimestampMillis: Long = 0L, /** Instance ID for logging purposes */ - val instanceId: InstanceId, + val instanceId: InstanceId = InstanceId.fakeInstanceId(-1), /** The UID of the app, used for logging */ - val appUid: Int, + val appUid: Int = Process.INVALID_UID, /** Whether explicit indicator exists */ val isExplicit: Boolean = false, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt index 52c605f55665..b44658502f48 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt @@ -30,23 +30,23 @@ import com.android.internal.logging.InstanceId /** State of a Smartspace media recommendations view. */ data class SmartspaceMediaData( /** Unique id of a Smartspace media target. */ - val targetId: String, + val targetId: String = "INVALID", /** Indicates if the status is active. */ - val isActive: Boolean, + val isActive: Boolean = false, /** Package name of the media recommendations' provider-app. */ - val packageName: String, + val packageName: String = "INVALID", /** Action to perform when the card is tapped. Also contains the target's extra info. */ - val cardAction: SmartspaceAction?, + val cardAction: SmartspaceAction? = null, /** List of media recommendations. */ - val recommendations: List<SmartspaceAction>, + val recommendations: List<SmartspaceAction> = emptyList(), /** Intent for the user's initiated dismissal. */ - val dismissIntent: Intent?, + val dismissIntent: Intent? = null, /** The timestamp in milliseconds that the card was generated */ - val headphoneConnectionTimeMillis: Long, + val headphoneConnectionTimeMillis: Long = 0L, /** Instance ID for [MediaUiEventLogger] */ - val instanceId: InstanceId, + val instanceId: InstanceId? = null, /** The timestamp in milliseconds indicating when the card should be removed */ - val expiryTimeMs: Long, + val expiryTimeMs: Long = 0L, ) { /** * Indicates if all the data is valid. diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt index ba7d41008a01..963c602b3d1e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt @@ -18,19 +18,13 @@ package com.android.systemui.media.controls.ui.controller import android.content.Context import android.content.res.Configuration -import android.database.ContentObserver -import android.net.Uri -import android.os.Handler -import android.os.UserHandle -import android.provider.Settings import android.view.View import android.view.ViewGroup import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.dagger.MediaModule.KEYGUARD @@ -43,7 +37,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.asIndenting import com.android.systemui.util.println -import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.withIncreasedIndent import java.io.PrintWriter import javax.inject.Inject @@ -61,8 +54,6 @@ constructor( private val bypassController: KeyguardBypassController, private val statusBarStateController: SysuiStatusBarStateController, private val context: Context, - private val secureSettings: SecureSettings, - @Main private val handler: Handler, configurationController: ConfigurationController, private val splitShadeStateController: SplitShadeStateController, private val logger: KeyguardMediaControllerLogger, @@ -91,26 +82,6 @@ constructor( } ) - val settingsObserver: ContentObserver = - object : ContentObserver(handler) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - if (uri == lockScreenMediaPlayerUri) { - allowMediaPlayerOnLockScreen = - secureSettings.getBoolForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - true, - UserHandle.USER_CURRENT - ) - refreshMediaPosition(reason = "allowMediaPlayerOnLockScreen changed") - } - } - } - secureSettings.registerContentObserverForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - settingsObserver, - UserHandle.USER_ALL - ) - // First let's set the desired state that we want for this host mediaHost.expansion = MediaHostState.EXPANDED mediaHost.showsOnlyActiveMedia = true @@ -156,16 +127,6 @@ constructor( private set private var splitShadeContainer: ViewGroup? = null - /** Track the media player setting status on lock screen. */ - private var allowMediaPlayerOnLockScreen: Boolean = - secureSettings.getBoolForUser( - Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, - true, - UserHandle.USER_CURRENT - ) - private val lockScreenMediaPlayerUri = - secureSettings.getUriFor(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) - /** * Attaches media container in single pane mode, situated at the top of the notifications list */ @@ -185,7 +146,7 @@ constructor( refreshMediaPosition(reason = "onMediaHostVisibilityChanged") if (visible) { - if (migrateClocksToBlueprint() && useSplitShade) { + if (MigrateClocksToBlueprint.isEnabled && useSplitShade) { return } mediaHost.hostView.layoutParams.apply { @@ -229,14 +190,12 @@ constructor( // mediaHost.visible required for proper animations handling val isMediaHostVisible = mediaHost.visible val isBypassNotEnabled = !bypassController.bypassEnabled - val currentAllowMediaPlayerOnLockScreen = allowMediaPlayerOnLockScreen val useSplitShade = useSplitShade val shouldBeVisibleForSplitShade = shouldBeVisibleForSplitShade() visible = isMediaHostVisible && isBypassNotEnabled && keyguardOrUserSwitcher && - currentAllowMediaPlayerOnLockScreen && shouldBeVisibleForSplitShade logger.logRefreshMediaPosition( reason = reason, @@ -246,7 +205,6 @@ constructor( keyguardOrUserSwitcher = keyguardOrUserSwitcher, mediaHostVisible = isMediaHostVisible, bypassNotEnabled = isBypassNotEnabled, - currentAllowMediaPlayerOnLockScreen = currentAllowMediaPlayerOnLockScreen, shouldBeVisibleForSplitShade = shouldBeVisibleForSplitShade, ) val currActiveContainer = activeContainer @@ -321,7 +279,6 @@ constructor( println("Self", this@KeyguardMediaController) println("visible", visible) println("useSplitShade", useSplitShade) - println("allowMediaPlayerOnLockScreen", allowMediaPlayerOnLockScreen) println("bypassController.bypassEnabled", bypassController.bypassEnabled) println("isDozeWakeUpAnimationWaiting", isDozeWakeUpAnimationWaiting) println("singlePaneContainer", singlePaneContainer) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerLogger.kt index c0d9dc23a6d5..4d1827efe82f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerLogger.kt @@ -36,7 +36,6 @@ constructor(@KeyguardMediaControllerLog private val logBuffer: LogBuffer) { keyguardOrUserSwitcher: Boolean, mediaHostVisible: Boolean, bypassNotEnabled: Boolean, - currentAllowMediaPlayerOnLockScreen: Boolean, shouldBeVisibleForSplitShade: Boolean, ) { logBuffer.log( @@ -50,8 +49,7 @@ constructor(@KeyguardMediaControllerLog private val logBuffer: LogBuffer) { bool3 = keyguardOrUserSwitcher bool4 = mediaHostVisible int2 = if (bypassNotEnabled) 1 else 0 - str2 = currentAllowMediaPlayerOnLockScreen.toString() - str3 = shouldBeVisibleForSplitShade.toString() + str2 = shouldBeVisibleForSplitShade.toString() }, { "refreshMediaPosition(reason=$str1, " + @@ -60,8 +58,7 @@ constructor(@KeyguardMediaControllerLog private val logBuffer: LogBuffer) { "keyguardOrUserSwitcher=$bool3, " + "mediaHostVisible=$bool4, " + "bypassNotEnabled=${int2 == 1}, " + - "currentAllowMediaPlayerOnLockScreen=$str2, " + - "shouldBeVisibleForSplitShade=$str3)" + "shouldBeVisibleForSplitShade=$str2)" } ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index b721236eab01..c3c1e83546df 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -22,6 +22,7 @@ import android.content.Intent import android.content.res.ColorStateList import android.content.res.Configuration import android.database.ContentObserver +import android.os.UserHandle import android.provider.Settings import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS import android.util.Log @@ -44,6 +45,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.media.controls.domain.pipeline.MediaDataManager @@ -76,6 +78,8 @@ import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import java.util.Locale @@ -83,10 +87,16 @@ import java.util.TreeMap import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Provider +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext private const val TAG = "MediaCarouselController" private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) @@ -108,6 +118,7 @@ constructor( private val systemClock: SystemClock, @Main executor: DelayableExecutor, @Background private val bgExecutor: Executor, + @Background private val backgroundDispatcher: CoroutineDispatcher, private val mediaManager: MediaDataManager, configurationController: ConfigurationController, falsingManager: FalsingManager, @@ -118,6 +129,7 @@ constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val globalSettings: GlobalSettings, + private val secureSettings: SecureSettings, ) : Dumpable { /** The current width of the carousel */ var currentCarouselWidth: Int = 0 @@ -191,6 +203,8 @@ constructor( } } + private var allowMediaPlayerOnLockScreen = false + /** Whether the media card currently has the "expanded" layout */ @VisibleForTesting var currentlyExpanded = true @@ -532,8 +546,9 @@ constructor( keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) mediaCarousel.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { - // A backup to show media carousel (if available) once the keyguard is gone. listenForAnyStateToGoneKeyguardTransition(this) + listenForAnyStateToLockscreenTransition(this) + listenForLockscreenSettingChanges(this) } } @@ -587,7 +602,49 @@ constructor( return scope.launch { keyguardTransitionInteractor.anyStateToGoneTransition .filter { it.transitionState == TransitionState.FINISHED } - .collect { showMediaCarousel() } + .collect { + showMediaCarousel() + updateHostVisibility() + } + } + } + + @VisibleForTesting + internal fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor.anyStateToLockscreenTransition + .filter { it.transitionState == TransitionState.FINISHED } + .collect { + if (!allowMediaPlayerOnLockScreen) { + updateHostVisibility() + } + } + } + } + + @VisibleForTesting + internal fun listenForLockscreenSettingChanges(scope: CoroutineScope): Job { + return scope.launch { + secureSettings + .observerFlow(UserHandle.USER_ALL, Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN) + // query to get initial value + .onStart { emit(Unit) } + .map { getMediaLockScreenSetting() } + .distinctUntilChanged() + .collectLatest { + allowMediaPlayerOnLockScreen = it + updateHostVisibility() + } + } + } + + private suspend fun getMediaLockScreenSetting(): Boolean { + return withContext(backgroundDispatcher) { + secureSettings.getBoolForUser( + Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, + true, + UserHandle.USER_CURRENT + ) } } @@ -600,6 +657,13 @@ constructor( updatePlayers(recreateMedia = true) } + /** Return true if the carousel should be hidden because lockscreen is currently visible */ + fun isLockedAndHidden(): Boolean { + val keyguardState = keyguardTransitionInteractor.getFinishedState() + return !allowMediaPlayerOnLockScreen && + KeyguardState.lockscreenVisibleInState(keyguardState) + } + private fun reorderAllPlayers( previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?, key: String? = null @@ -1163,7 +1227,7 @@ constructor( // Only log media resume card when Smartspace data is available if ( !mediaControlKey.isSsMediaRec && - !mediaManager.smartspaceMediaData.isActive && + !mediaManager.isRecommendationActive() && MediaPlayerData.smartspaceMediaData == null ) { return diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java index 26c63f31fa46..899b9ed103cd 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaControlPanel.java @@ -1306,7 +1306,7 @@ public class MediaControlPanel { TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z, // Color will be correctly updated in ColorSchemeTransition. /* color= */ mColorSchemeTransition.getAccentPrimary().getCurrentColor(), - /* backgroundColor= */ Color.BLACK, + /* screenColor= */ Color.BLACK, width, height, TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt index d92168bf9fa4..eca76b603b1a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaHost.kt @@ -23,6 +23,7 @@ import android.view.View.OnAttachStateChangeListener import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.ui.controller.MediaCarouselController import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.controller.MediaHostStatesManager import com.android.systemui.media.controls.ui.controller.MediaLocation @@ -33,12 +34,12 @@ import com.android.systemui.util.animation.UniqueObjectHostView import java.util.Objects import javax.inject.Inject -class MediaHost -constructor( +class MediaHost( private val state: MediaHostStateHolder, private val mediaHierarchyManager: MediaHierarchyManager, private val mediaDataManager: MediaDataManager, - private val mediaHostStatesManager: MediaHostStatesManager + private val mediaHostStatesManager: MediaHostStatesManager, + private val mediaCarouselController: MediaCarouselController, ) : MediaHostState by state { lateinit var hostView: UniqueObjectHostView var location: Int = -1 @@ -202,7 +203,9 @@ constructor( */ fun updateViewVisibility() { state.visible = - if (showsOnlyActiveMedia) { + if (mediaCarouselController.isLockedAndHidden()) { + false + } else if (showsOnlyActiveMedia) { mediaDataManager.hasActiveMediaOrRecommendation() } else { mediaDataManager.hasAnyMediaOrRecommendation() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt index f8c816ca0b52..2c25fe2ecb29 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt @@ -161,7 +161,7 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) logger.log(event) } - fun logRecommendationAdded(packageName: String, instanceId: InstanceId) { + fun logRecommendationAdded(packageName: String, instanceId: InstanceId?) { logger.logWithInstanceId( MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, 0, @@ -170,7 +170,7 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) ) } - fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) { + fun logRecommendationRemoved(packageName: String, instanceId: InstanceId?) { logger.logWithInstanceId( MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, 0, diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index d84e5dde6967..59b98b2792be 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -19,7 +19,9 @@ package com.android.systemui.media.dagger; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.LogBufferFactory; +import com.android.systemui.media.controls.domain.MediaDomainModule; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; +import com.android.systemui.media.controls.ui.controller.MediaCarouselController; import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager; import com.android.systemui.media.controls.ui.controller.MediaHostStatesManager; import com.android.systemui.media.controls.ui.view.MediaHost; @@ -38,7 +40,11 @@ import java.util.Optional; import javax.inject.Named; /** Dagger module for the media package. */ -@Module(subcomponents = { +@Module( + includes = { + MediaDomainModule.class + }, + subcomponents = { MediaComplicationComponent.class, }) public interface MediaModule { @@ -54,8 +60,9 @@ public interface MediaModule { @Named(QS_PANEL) static MediaHost providesQSMediaHost(MediaHost.MediaHostStateHolder stateHolder, MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, - MediaHostStatesManager statesManager) { - return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + MediaHostStatesManager statesManager, MediaCarouselController carouselController) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager, + carouselController); } /** */ @@ -64,8 +71,9 @@ public interface MediaModule { @Named(QUICK_QS_PANEL) static MediaHost providesQuickQSMediaHost(MediaHost.MediaHostStateHolder stateHolder, MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, - MediaHostStatesManager statesManager) { - return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + MediaHostStatesManager statesManager, MediaCarouselController carouselController) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager, + carouselController); } /** */ @@ -74,8 +82,9 @@ public interface MediaModule { @Named(KEYGUARD) static MediaHost providesKeyguardMediaHost(MediaHost.MediaHostStateHolder stateHolder, MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, - MediaHostStatesManager statesManager) { - return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + MediaHostStatesManager statesManager, MediaCarouselController carouselController) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager, + carouselController); } /** */ @@ -84,8 +93,9 @@ public interface MediaModule { @Named(DREAM) static MediaHost providesDreamMediaHost(MediaHost.MediaHostStateHolder stateHolder, MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, - MediaHostStatesManager statesManager) { - return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + MediaHostStatesManager statesManager, MediaCarouselController carouselController) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager, + carouselController); } /** */ @@ -94,8 +104,9 @@ public interface MediaModule { @Named(COMMUNAL_HUB) static MediaHost providesCommunalMediaHost(MediaHost.MediaHostStateHolder stateHolder, MediaHierarchyManager hierarchyManager, MediaDataManager dataManager, - MediaHostStatesManager statesManager) { - return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager); + MediaHostStatesManager statesManager, MediaCarouselController carouselController) { + return new MediaHost(stateHolder, hierarchyManager, dataManager, statesManager, + carouselController); } /** Provides a logging buffer related to the media tap-to-transfer chip on the sender device. */ diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java index dfe41eb9f7f2..d49a513f6e9f 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java @@ -243,7 +243,7 @@ public final class NavBarHelper implements Settings.Secure.getUriFor(Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED), false, mAssistContentObserver, UserHandle.USER_ALL); mContentResolver.registerContentObserver( - Settings.Secure.getUriFor(Secure.SEARCH_LONG_PRESS_HOME_ENABLED), + Settings.Secure.getUriFor(Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED), false, mAssistContentObserver, UserHandle.USER_ALL); mContentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ASSIST_TOUCH_GESTURE_ENABLED), @@ -443,10 +443,10 @@ public final class NavBarHelper implements boolean overrideLongPressHome = mAssistManagerLazy.get() .shouldOverrideAssist(AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); boolean longPressDefault = mContext.getResources().getBoolean(overrideLongPressHome - ? com.android.internal.R.bool.config_searchLongPressHomeEnabledDefault + ? com.android.internal.R.bool.config_searchAllEntrypointsEnabledDefault : com.android.internal.R.bool.config_assistLongPressHomeEnabledDefault); mLongPressHomeEnabled = Settings.Secure.getIntForUser(mContentResolver, - overrideLongPressHome ? Secure.SEARCH_LONG_PRESS_HOME_ENABLED + overrideLongPressHome ? Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED : Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED, longPressDefault ? 1 : 0, mUserTracker.getUserId()) != 0; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 768bb8e2e917..4fe3a11078db 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -934,48 +934,51 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private void orientSecondaryHomeHandle() { if (!canShowSecondaryHandle()) { - if (mStartingQuickSwitchRotation == -1) { - resetSecondaryHandle(); - } return; } - int deltaRotation = deltaRotation(mCurrentRotation, mStartingQuickSwitchRotation); - if (mStartingQuickSwitchRotation == -1 || deltaRotation == -1) { - // Curious if starting quickswitch can change between the if check and our delta - Log.d(TAG, "secondary nav delta rotation: " + deltaRotation - + " current: " + mCurrentRotation - + " starting: " + mStartingQuickSwitchRotation); - } - int height = 0; - int width = 0; - Rect dispSize = mWindowManager.getCurrentWindowMetrics().getBounds(); - mOrientationHandle.setDeltaRotation(deltaRotation); - switch (deltaRotation) { - case Surface.ROTATION_90, Surface.ROTATION_270: - height = dispSize.height(); - width = mView.getHeight(); - break; - case Surface.ROTATION_180, Surface.ROTATION_0: - // TODO(b/152683657): Need to determine best UX for this - if (!mShowOrientedHandleForImmersiveMode) { - resetSecondaryHandle(); - return; - } - width = dispSize.width(); - height = mView.getHeight(); - break; - } + if (mStartingQuickSwitchRotation == -1) { + resetSecondaryHandle(); + } else { + int deltaRotation = deltaRotation(mCurrentRotation, mStartingQuickSwitchRotation); + if (mStartingQuickSwitchRotation == -1 || deltaRotation == -1) { + // Curious if starting quickswitch can change between the if check and our delta + Log.d(TAG, "secondary nav delta rotation: " + deltaRotation + + " current: " + mCurrentRotation + + " starting: " + mStartingQuickSwitchRotation); + } + int height = 0; + int width = 0; + Rect dispSize = mWindowManager.getCurrentWindowMetrics().getBounds(); + mOrientationHandle.setDeltaRotation(deltaRotation); + switch (deltaRotation) { + case Surface.ROTATION_90: + case Surface.ROTATION_270: + height = dispSize.height(); + width = mView.getHeight(); + break; + case Surface.ROTATION_180: + case Surface.ROTATION_0: + // TODO(b/152683657): Need to determine best UX for this + if (!mShowOrientedHandleForImmersiveMode) { + resetSecondaryHandle(); + return; + } + width = dispSize.width(); + height = mView.getHeight(); + break; + } - mOrientationParams.gravity = - deltaRotation == Surface.ROTATION_0 ? Gravity.BOTTOM : - (deltaRotation == Surface.ROTATION_90 ? Gravity.LEFT : Gravity.RIGHT); - mOrientationParams.height = height; - mOrientationParams.width = width; - mWindowManager.updateViewLayout(mOrientationHandle, mOrientationParams); - mView.setVisibility(View.GONE); - mOrientationHandle.setVisibility(View.VISIBLE); - logNavbarOrientation("orientSecondaryHomeHandle"); + mOrientationParams.gravity = + deltaRotation == Surface.ROTATION_0 ? Gravity.BOTTOM : + (deltaRotation == Surface.ROTATION_90 ? Gravity.LEFT : Gravity.RIGHT); + mOrientationParams.height = height; + mOrientationParams.width = width; + mWindowManager.updateViewLayout(mOrientationHandle, mOrientationParams); + mView.setVisibility(View.GONE); + mOrientationHandle.setVisibility(View.VISIBLE); + logNavbarOrientation("orientSecondaryHomeHandle"); + } } private void resetSecondaryHandle() { @@ -1789,8 +1792,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } private boolean canShowSecondaryHandle() { - return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null - && mStartingQuickSwitchRotation != -1; + return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null; } private final UserTracker.Callback mUserChangedCallback = diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java index 1d820a14be4e..0a880293ca76 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java @@ -21,6 +21,9 @@ import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS; import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN; +import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD; +import static android.appwidget.flags.Flags.generatedPreviews; import static android.content.Intent.ACTION_BOOT_COMPLETED; import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.ACTION_PACKAGE_REMOVED; @@ -56,6 +59,7 @@ import android.app.people.IPeopleManager; import android.app.people.PeopleManager; import android.app.people.PeopleSpaceTile; import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -80,12 +84,15 @@ import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; import android.text.TextUtils; import android.util.Log; +import android.util.SparseBooleanArray; import android.widget.RemoteViews; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.UiEventLoggerImpl; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; @@ -96,6 +103,8 @@ import com.android.systemui.people.PeopleBackupFollowUpJob; import com.android.systemui.people.PeopleSpaceUtils; import com.android.systemui.people.PeopleTileViewHelper; import com.android.systemui.people.SharedPreferencesHelper; +import com.android.systemui.res.R; +import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationListener.NotificationHandler; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -160,13 +169,27 @@ public class PeopleSpaceWidgetManager implements Dumpable { @GuardedBy("mLock") public static Map<Integer, PeopleSpaceTile> mTiles = new HashMap<>(); + @NonNull private final UserTracker mUserTracker; + @NonNull private final SparseBooleanArray mUpdatedPreviews = new SparseBooleanArray(); + @NonNull private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = + new KeyguardUpdateMonitorCallback() { + @Override + public void onUserUnlocked() { + if (DEBUG) { + Log.d(TAG, "onUserUnlocked " + mUserTracker.getUserId()); + } + updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); + } + }; + @Inject public PeopleSpaceWidgetManager(Context context, LauncherApps launcherApps, CommonNotifCollection notifCollection, PackageManager packageManager, Optional<Bubbles> bubblesOptional, UserManager userManager, NotificationManager notificationManager, BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor, - DumpManager dumpManager) { + DumpManager dumpManager, @NonNull UserTracker userTracker, + @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor) { if (DEBUG) Log.d(TAG, "constructor"); mContext = context; mAppWidgetManager = AppWidgetManager.getInstance(context); @@ -187,6 +210,8 @@ public class PeopleSpaceWidgetManager implements Dumpable { mBroadcastDispatcher = broadcastDispatcher; mBgExecutor = bgExecutor; dumpManager.registerNormalDumpable(TAG, this); + mUserTracker = userTracker; + keyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); } /** Initializes {@PeopleSpaceWidgetManager}. */ @@ -246,7 +271,7 @@ public class PeopleSpaceWidgetManager implements Dumpable { CommonNotifCollection notifCollection, PackageManager packageManager, Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager, INotificationManager iNotificationManager, NotificationManager notificationManager, - @Background Executor executor) { + @Background Executor executor, UserTracker userTracker) { mContext = context; mAppWidgetManager = appWidgetManager; mIPeopleManager = iPeopleManager; @@ -262,6 +287,7 @@ public class PeopleSpaceWidgetManager implements Dumpable { mManager = this; mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); mBgExecutor = executor; + mUserTracker = userTracker; } /** @@ -1407,4 +1433,32 @@ public class PeopleSpaceWidgetManager implements Dumpable { Trace.traceEnd(Trace.TRACE_TAG_APP); } + + @VisibleForTesting + void updateGeneratedPreviewForUser(UserHandle user) { + if (!generatedPreviews() || mUpdatedPreviews.get(user.getIdentifier()) + || !mUserManager.isUserUnlocked(user)) { + return; + } + + // The widget provider may be disabled on SystemUI implementers, e.g. TvSystemUI. + ComponentName provider = new ComponentName(mContext, PeopleSpaceWidgetProvider.class); + List<AppWidgetProviderInfo> infos = mAppWidgetManager.getInstalledProvidersForPackage( + mContext.getPackageName(), user); + if (infos.stream().noneMatch(info -> info.provider.equals(provider))) { + return; + } + + if (DEBUG) { + Log.d(TAG, "Updating People Space widget preview for user " + user.getIdentifier()); + } + boolean success = mAppWidgetManager.setWidgetPreview( + provider, WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD, + new RemoteViews(mContext.getPackageName(), + R.layout.people_space_placeholder_layout)); + if (DEBUG && !success) { + Log.d(TAG, "Failed to update generated preview for user " + user.getIdentifier()); + } + mUpdatedPreviews.put(user.getIdentifier(), success); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 5d2aeef5eb16..b34b3701528b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -432,6 +432,9 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { for (int i = 0; i < NP; i++) { mPages.get(i).removeAllViews(); } + if (mPageIndicator != null) { + mPageIndicator.setNumPages(numPages); + } if (NP == numPages) { return; } @@ -443,7 +446,6 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { mLogger.d("Removing page"); mPages.remove(mPages.size() - 1); } - mPageIndicator.setNumPages(mPages.size()); setAdapter(mAdapter); mAdapter.notifyDataSetChanged(); if (mPageToRestore != NO_PAGE) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 7a7ee59fa63f..00757b7bd51a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -127,8 +127,9 @@ public class QSPanel extends LinearLayout implements Tunable { } - void initialize(QSLogger qsLogger) { + void initialize(QSLogger qsLogger, boolean usingMediaPlayer) { mQsLogger = qsLogger; + mUsingMediaPlayer = usingMediaPlayer; mTileLayout = getOrCreateTileLayout(); if (mUsingMediaPlayer) { @@ -163,22 +164,25 @@ public class QSPanel extends LinearLayout implements Tunable { } protected void setHorizontalContentContainerClipping() { - mHorizontalContentContainer.setClipChildren(true); - mHorizontalContentContainer.setClipToPadding(false); - // Don't clip on the top, that way, secondary pages tiles can animate up - // Clipping coordinates should be relative to this view, not absolute (parent coordinates) - mHorizontalContentContainer.addOnLayoutChangeListener( - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - if ((right - left) != (oldRight - oldLeft) - || ((bottom - top) != (oldBottom - oldTop))) { - mClippingRect.right = right - left; - mClippingRect.bottom = bottom - top; - mHorizontalContentContainer.setClipBounds(mClippingRect); - } - }); - mClippingRect.left = 0; - mClippingRect.top = -1000; - mHorizontalContentContainer.setClipBounds(mClippingRect); + if (mHorizontalContentContainer != null) { + mHorizontalContentContainer.setClipChildren(true); + mHorizontalContentContainer.setClipToPadding(false); + // Don't clip on the top, that way, secondary pages tiles can animate up + // Clipping coordinates should be relative to this view, not absolute + // (parent coordinates) + mHorizontalContentContainer.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if ((right - left) != (oldRight - oldLeft) + || ((bottom - top) != (oldBottom - oldTop))) { + mClippingRect.right = right - left; + mClippingRect.bottom = bottom - top; + mHorizontalContentContainer.setClipBounds(mClippingRect); + } + }); + mClippingRect.left = 0; + mClippingRect.top = -1000; + mHorizontalContentContainer.setClipBounds(mClippingRect); + } } /** @@ -412,7 +416,7 @@ public class QSPanel extends LinearLayout implements Tunable { } private void updateHorizontalLinearLayoutMargins() { - if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) { + if (mUsingMediaPlayer && mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) { LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams(); lp.bottomMargin = Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0); mHorizontalLinearLayout.setLayoutParams(lp); @@ -461,6 +465,11 @@ public class QSPanel extends LinearLayout implements Tunable { /** Call when orientation has changed and MediaHost needs to be adjusted. */ private void reAttachMediaHost(ViewGroup hostView, boolean horizontal) { if (!mUsingMediaPlayer) { + // If the host view was attached, detach it. + ViewGroup parent = (ViewGroup) hostView.getParent(); + if (parent != null) { + parent.removeView(hostView); + } return; } mMediaHostView = hostView; @@ -492,8 +501,10 @@ public class QSPanel extends LinearLayout implements Tunable { public void setExpanded(boolean expanded) { if (mExpanded == expanded) return; mExpanded = expanded; - if (!mExpanded && mTileLayout instanceof PagedTileLayout) { - ((PagedTileLayout) mTileLayout).setCurrentItem(0, false); + if (!mExpanded && mTileLayout instanceof PagedTileLayout tilesLayout) { + // Use post, so it will wait until the view is attached. If the view is not attached, + // it will not populate corresponding views (and will not do it later when attached). + tilesLayout.post(() -> tilesLayout.setCurrentItem(0, false)); } } @@ -616,7 +627,10 @@ public class QSPanel extends LinearLayout implements Tunable { if (horizontal != mUsingHorizontalLayout || force) { Log.d(getDumpableTag(), "setUsingHorizontalLayout: " + horizontal + ", " + force); mUsingHorizontalLayout = horizontal; - ViewGroup newParent = horizontal ? mHorizontalContentContainer : this; + // The tile layout should be reparented if horizontal and we are using media. If not + // using media, the parent should always be this. + ViewGroup newParent = + horizontal && mUsingMediaPlayer ? mHorizontalContentContainer : this; switchAllContentToParent(newParent, mTileLayout); reAttachMediaHost(mediaHostView, horizontal); if (needsDynamicRowsAndColumns()) { @@ -624,7 +638,9 @@ public class QSPanel extends LinearLayout implements Tunable { mTileLayout.setMaxColumns(horizontal ? 2 : 4); } updateMargins(mediaHostView); - mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE); + if (mHorizontalLinearLayout != null) { + mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 5e12b9d4cc34..d8e81875bbbf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -167,7 +167,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr @Override protected void onInit() { - mView.initialize(mQSLogger); + mView.initialize(mQSLogger, mUsingMediaPlayer); mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), ""); mHost.addCallback(mQSHostCallback); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 63963ded2923..e1c543f8f025 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -37,7 +37,6 @@ import android.util.TypedValue import android.view.Gravity import android.view.LayoutInflater import android.view.View -import android.view.ViewConfiguration import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo @@ -185,6 +184,8 @@ open class QSTileViewImpl @JvmOverloads constructor( private var initialLongPressProperties: QSLongPressProperties? = null private var finalLongPressProperties: QSLongPressProperties? = null private val colorEvaluator = ArgbEvaluator.getInstance() + val hasLongPressEffect: Boolean + get() = longPressEffect != null init { val typedValue = TypedValue() @@ -611,10 +612,9 @@ open class QSTileViewImpl @JvmOverloads constructor( // Long-press effects if (quickSettingsVisualHapticsLongpress()){ - if (state.handlesLongClick) { - // initialize the long-press effect and set it as the touch listener + if (state.handlesLongClick && maybeCreateAndInitializeLongPressEffect()) { + // set the valid long-press effect as the touch listener showRippleEffect = false - initializeLongPressEffect() setOnTouchListener(longPressEffect) QSLongPressEffectViewBinder.bind(this, longPressEffect) } else { @@ -751,7 +751,7 @@ open class QSTileViewImpl @JvmOverloads constructor( override fun onActivityLaunchAnimationEnd() = resetLongPressEffectProperties() fun updateLongPressEffectProperties(effectProgress: Float) { - if (!isLongClickable) return + if (!isLongClickable || longPressEffect == null) return setAllColors( colorEvaluator.evaluate( effectProgress, @@ -836,13 +836,25 @@ open class QSTileViewImpl @JvmOverloads constructor( icon.setTint(icon.mIcon as ImageView, lastIconTint) } - private fun initializeLongPressEffect() { + private fun maybeCreateAndInitializeLongPressEffect(): Boolean { + // Don't setup the effect if the long-press duration is invalid + val effectDuration = longPressEffectDuration + if (effectDuration <= 0) { + longPressEffect = null + return false + } + initializeLongPressProperties() - longPressEffect = - QSLongPressEffect( - vibratorHelper, - ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout(), - ) + if (longPressEffect == null) { + longPressEffect = + QSLongPressEffect( + vibratorHelper, + effectDuration, + ) + } else { + longPressEffect?.resetWithDuration(effectDuration) + } + return true } private fun initializeLongPressProperties() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java index 18d2f306c247..b0707db0d02d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -111,7 +111,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { @Override protected void handleClick(@Nullable View view) { if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) { - mDialogViewModel.showDialog(mContext, view); + mDialogViewModel.showDialog(view); } else { // Secondary clicks are header clicks, just toggle. final boolean isEnabled = mState.value; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt index d82b1755ac80..b418a174d84e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt @@ -44,6 +44,7 @@ import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.recordissue.IssueRecordingService +import com.android.systemui.recordissue.IssueRecordingState import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.res.R import com.android.systemui.screenrecord.RecordingService @@ -69,6 +70,7 @@ constructor( private val dialogTransitionAnimator: DialogTransitionAnimator, private val panelInteractor: PanelInteractor, private val userContextProvider: UserContextProvider, + private val issueRecordingState: IssueRecordingState, private val delegateFactory: RecordIssueDialogDelegate.Factory, ) : QSTileImpl<QSTile.BooleanState>( @@ -83,7 +85,16 @@ constructor( qsLogger ) { - @VisibleForTesting var isRecording: Boolean = false + private val onRecordingChangeListener = Runnable { refreshState() } + + override fun handleSetListening(listening: Boolean) { + super.handleSetListening(listening) + if (listening) { + issueRecordingState.addListener(onRecordingChangeListener) + } else { + issueRecordingState.removeListener(onRecordingChangeListener) + } + } override fun getTileLabel(): CharSequence = mContext.getString(R.string.qs_record_issue_label) @@ -103,13 +114,11 @@ constructor( @VisibleForTesting public override fun handleClick(view: View?) { - if (isRecording) { - isRecording = false + if (issueRecordingState.isRecording) { stopIssueRecordingService() } else { mUiHandler.post { showPrompt(view) } } - refreshState() } private fun startIssueRecordingService(screenRecord: Boolean, winscopeTracing: Boolean) = @@ -138,11 +147,9 @@ constructor( val dialog: AlertDialog = delegateFactory .create { - isRecording = true startIssueRecordingService(it.screenRecord, it.winscopeTracing) dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations() panelInteractor.collapsePanels() - refreshState() } .createDialog() val dismissAction = @@ -168,7 +175,7 @@ constructor( @VisibleForTesting public override fun handleUpdateState(qsTileState: QSTile.BooleanState, arg: Any?) { qsTileState.apply { - if (isRecording) { + if (issueRecordingState.isRecording) { value = true state = Tile.STATE_ACTIVE forceExpandIcon = false diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt index 1247854da61d..59fc81c82df0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt @@ -19,8 +19,6 @@ package com.android.systemui.qs.tiles.dialog.bluetooth import android.util.Log import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map /** Interactor class responsible for interacting with the Bluetooth Auto-On feature. */ @SysUISingleton @@ -30,14 +28,10 @@ constructor( private val bluetoothAutoOnRepository: BluetoothAutoOnRepository, ) { - val isEnabled = bluetoothAutoOnRepository.isAutoOn.map { it == ENABLED }.distinctUntilChanged() + val isEnabled = bluetoothAutoOnRepository.isAutoOn - /** - * Checks if the auto on value is present in the repository. - * - * @return `true` if a value is present (i.e, the feature is enabled by the Bluetooth server). - */ - suspend fun isValuePresent(): Boolean = bluetoothAutoOnRepository.isValuePresent() + /** Checks if the auto on feature is supported. */ + suspend fun isAutoOnSupported(): Boolean = bluetoothAutoOnRepository.isAutoOnSupported() /** * Sets enabled or disabled based on the provided value. @@ -45,17 +39,14 @@ constructor( * @param value `true` to enable the feature, `false` to disable it. */ suspend fun setEnabled(value: Boolean) { - if (!isValuePresent()) { + if (!isAutoOnSupported()) { Log.e(TAG, "Trying to set toggle value while feature not available.") } else { - val newValue = if (value) ENABLED else DISABLED - bluetoothAutoOnRepository.setAutoOn(newValue) + bluetoothAutoOnRepository.setAutoOn(value) } } companion object { private const val TAG = "BluetoothAutoOnInteractor" - const val DISABLED = 0 - const val ENABLED = 1 } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt index f97fc389b12c..9ee582a77862 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt @@ -16,22 +16,23 @@ package com.android.systemui.qs.tiles.dialog.bluetooth +import android.bluetooth.BluetoothAdapter +import android.util.Log +import com.android.settingslib.bluetooth.BluetoothCallback +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.user.data.repository.UserRepository -import com.android.systemui.util.settings.SecureSettings -import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -44,61 +45,87 @@ import kotlinx.coroutines.withContext class BluetoothAutoOnRepository @Inject constructor( - private val secureSettings: SecureSettings, - private val userRepository: UserRepository, + localBluetoothManager: LocalBluetoothManager?, + private val bluetoothAdapter: BluetoothAdapter?, @Application private val coroutineScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, ) { - // Flow representing the auto on setting value for the current user - @OptIn(ExperimentalCoroutinesApi::class) - internal val isAutoOn: StateFlow<Int> = - userRepository.selectedUserInfo - .flatMapLatest { userInfo -> - secureSettings - .observerFlow(userInfo.id, SETTING_NAME) - .onStart { emit(Unit) } - .map { secureSettings.getIntForUser(SETTING_NAME, UNSET, userInfo.id) } - } - .distinctUntilChanged() - .flowOn(backgroundDispatcher) - .stateIn( - coroutineScope, - SharingStarted.WhileSubscribed(replayExpirationMillis = 0), - UNSET - ) + // Flow representing the auto on state for the current user + internal val isAutoOn: Flow<Boolean> = + localBluetoothManager?.eventManager?.let { eventManager -> + conflatedCallbackFlow { + val listener = + object : BluetoothCallback { + override fun onAutoOnStateChanged(autoOnState: Int) { + super.onAutoOnStateChanged(autoOnState) + if ( + autoOnState == BluetoothAdapter.AUTO_ON_STATE_ENABLED || + autoOnState == BluetoothAdapter.AUTO_ON_STATE_DISABLED + ) { + trySendWithFailureLogging( + autoOnState == BluetoothAdapter.AUTO_ON_STATE_ENABLED, + TAG, + "onAutoOnStateChanged" + ) + } + } + } + eventManager.registerCallback(listener) + awaitClose { eventManager.unregisterCallback(listener) } + } + .onStart { emit(isAutoOnEnabled()) } + .flowOn(backgroundDispatcher) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(replayExpirationMillis = 0), + initialValue = false + ) + } + ?: flowOf(false) /** - * Checks if the auto on setting value is ever set for the current user. + * Checks if the auto on feature is supported for the current user. * - * @return `true` if the setting value is not UNSET, `false` otherwise. + * @throws Exception if an error occurs while checking auto-on support. */ - suspend fun isValuePresent(): Boolean = + suspend fun isAutoOnSupported(): Boolean = withContext(backgroundDispatcher) { - secureSettings.getIntForUser( - SETTING_NAME, - UNSET, - userRepository.getSelectedUserInfo().id - ) != UNSET + try { + bluetoothAdapter?.isAutoOnSupported ?: false + } catch (e: Exception) { + // Server could throw TimeoutException, InterruptedException or ExecutionException + Log.e(TAG, "Error calling isAutoOnSupported", e) + false + } } - /** - * Sets the Bluetooth Auto-On setting value for the current user. - * - * @param value The new setting value to be applied. - */ - suspend fun setAutoOn(value: Int) { + /** Sets the Bluetooth Auto-On for the current user. */ + suspend fun setAutoOn(value: Boolean) { withContext(backgroundDispatcher) { - secureSettings.putIntForUser( - SETTING_NAME, - value, - userRepository.getSelectedUserInfo().id - ) + try { + bluetoothAdapter?.setAutoOnEnabled(value) + } catch (e: Exception) { + // Server could throw IllegalStateException, TimeoutException, InterruptedException + // or ExecutionException + Log.e(TAG, "Error calling setAutoOnEnabled", e) + } } } - companion object { - const val SETTING_NAME = "bluetooth_automatic_turn_on" - const val UNSET = -1 + private suspend fun isAutoOnEnabled() = + withContext(backgroundDispatcher) { + try { + bluetoothAdapter?.isAutoOnEnabled ?: false + } catch (e: Exception) { + // Server could throw IllegalStateException, TimeoutException, InterruptedException + // or ExecutionException + Log.e(TAG, "Error calling isAutoOnEnabled", e) + false + } + } + + private companion object { + const val TAG = "BluetoothAutoOnRepository" } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt index 9d5370354fe8..a8d9e781228b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.tiles.dialog.bluetooth -import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -58,7 +57,6 @@ import kotlinx.coroutines.withContext class BluetoothTileDialogDelegate @AssistedInject internal constructor( - @Assisted private val context: Context, @Assisted private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties, @Assisted private val cachedContentHeight: Int, @Assisted private val bluetoothToggleInitialValue: Boolean, @@ -69,11 +67,8 @@ internal constructor( private val uiEventLogger: UiEventLogger, private val logger: BluetoothTileDialogLogger, private val systemuiDialogFactory: SystemUIDialog.Factory, - mainLayoutInflater: LayoutInflater, ) : SystemUIDialog.Delegate { - private val layoutInflater = mainLayoutInflater.cloneInContext(context) - private val mutableBluetoothStateToggle: MutableStateFlow<Boolean> = MutableStateFlow(bluetoothToggleInitialValue) internal val bluetoothStateToggle @@ -102,7 +97,6 @@ internal constructor( @AssistedFactory internal interface Factory { fun create( - context: Context, initialUiProperties: BluetoothTileDialogViewModel.UiProperties, cachedContentHeight: Int, bluetoothEnabled: Boolean, @@ -112,16 +106,15 @@ internal constructor( } override fun createDialog(): SystemUIDialog { - val dialog = systemuiDialogFactory.create(this, context) - - return dialog + return systemuiDialogFactory.create(this) } override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { SystemUIDialog.registerDismissListener(dialog, dismissListener) uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TILE_DIALOG_SHOWN) + val context = dialog.context - layoutInflater.inflate(R.layout.bluetooth_tile_dialog, null).apply { + LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null).apply { accessibilityPaneTitle = context.getText(R.string.accessibility_desc_quick_settings) dialog.setContentView(this) } @@ -201,7 +194,7 @@ internal constructor( setEnabled(true) alpha = ENABLED_ALPHA } - getSubtitleTextView(dialog).text = context.getString(uiProperties.subTitleResId) + getSubtitleTextView(dialog).text = dialog.context.getString(uiProperties.subTitleResId) getAutoOnToggleView(dialog).visibility = uiProperties.autoOnToggleVisibility } @@ -215,7 +208,7 @@ internal constructor( setEnabled(true) alpha = ENABLED_ALPHA } - getAutoOnToggleInfoTextView(dialog).text = context.getString(infoResId) + getAutoOnToggleInfoTextView(dialog).text = dialog.context.getString(infoResId) } private fun setupToggle(dialog: SystemUIDialog) { @@ -288,7 +281,7 @@ internal constructor( private fun setupRecyclerView(dialog: SystemUIDialog) { getDeviceListView(dialog).apply { - layoutManager = LinearLayoutManager(context) + layoutManager = LinearLayoutManager(dialog.context) adapter = deviceItemAdapter } } @@ -343,7 +336,9 @@ internal constructor( private val asyncListDiffer = AsyncListDiffer(this, diffUtilCallback) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder { - val view = layoutInflater.inflate(R.layout.bluetooth_device_item, parent, false) + val view = + LayoutInflater.from(parent.context) + .inflate(R.layout.bluetooth_device_item, parent, false) return DeviceItemViewHolder(view) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt index e4f3c199371e..fd624d2f1ba1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.tiles.dialog.bluetooth -import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.os.Bundle @@ -29,7 +28,6 @@ import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger -import com.android.settingslib.flags.Flags.bluetoothQsTileDialogAutoOnToggle import com.android.systemui.Prefs import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator @@ -78,19 +76,19 @@ constructor( /** * Shows the dialog. * - * @param context The context in which the dialog is displayed. * @param view The view from which the dialog is shown. */ @kotlinx.coroutines.ExperimentalCoroutinesApi - fun showDialog(context: Context, view: View?) { + fun showDialog(view: View?) { cancelJob() job = coroutineScope.launch(mainDispatcher) { var updateDeviceItemJob: Job? var updateDialogUiJob: Job? = null - val dialogDelegate = createBluetoothTileDialog(context) + val dialogDelegate = createBluetoothTileDialog() val dialog = dialogDelegate.createDialog() + val context = dialog.context view?.let { dialogTransitionAnimator.showFromView( @@ -213,7 +211,7 @@ constructor( } } - private suspend fun createBluetoothTileDialog(context: Context): BluetoothTileDialogDelegate { + private suspend fun createBluetoothTileDialog(): BluetoothTileDialogDelegate { val cachedContentHeight = withContext(backgroundDispatcher) { sharedPreferences.getInt( @@ -223,7 +221,6 @@ constructor( } return bluetoothDialogDelegateFactory.create( - context, UiProperties.build( bluetoothStateInteractor.isBluetoothEnabled, isAutoOnToggleFeatureAvailable() @@ -277,7 +274,7 @@ constructor( @VisibleForTesting internal suspend fun isAutoOnToggleFeatureAvailable() = - bluetoothQsTileDialogAutoOnToggle() && bluetoothAutoOnInteractor.isValuePresent() + bluetoothAutoOnInteractor.isAutoOnSupported() companion object { private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog" diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt index 2b8c335cb0ad..c0fc52e85866 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt @@ -83,6 +83,7 @@ constructor( } } + sideViewIcon = QSTileState.SideViewIcon.Chevron contentDescription = label supportedActions = setOf(QSTileState.UserAction.CLICK) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt index fc42ba495a51..b25c61cba2b7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt @@ -39,7 +39,7 @@ class DataSaverDialogDelegate( return sysuiDialogFactory.create(this, context) } - override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { with(dialog) { setTitle(R.string.data_saver_enable_title) setMessage(R.string.data_saver_description) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt new file mode 100644 index 000000000000..a2a9e87a5981 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.work.domain.interactor + +import android.os.UserHandle +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.statusbar.phone.ManagedProfileController +import com.android.systemui.util.kotlin.hasActiveWorkProfile +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** Observes data saver state changes providing the [WorkModeTileModel]. */ +class WorkModeTileDataInteractor +@Inject +constructor( + private val profileController: ManagedProfileController, +) : QSTileDataInteractor<WorkModeTileModel> { + override fun tileData( + user: UserHandle, + triggers: Flow<DataUpdateTrigger> + ): Flow<WorkModeTileModel> = + profileController.hasActiveWorkProfile.map { hasActiveWorkProfile: Boolean -> + if (hasActiveWorkProfile) { + WorkModeTileModel.HasActiveProfile(profileController.isWorkModeEnabled) + } else { + WorkModeTileModel.NoActiveProfile + } + } + + override fun availability(user: UserHandle): Flow<Boolean> = + profileController.hasActiveWorkProfile +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt new file mode 100644 index 000000000000..f765f8b3ac77 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.work.domain.interactor + +import android.content.Intent +import android.provider.Settings +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.interactor.QSTileInput +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import com.android.systemui.statusbar.phone.ManagedProfileController +import javax.inject.Inject + +/** Handles airplane mode tile clicks and long clicks. */ +class WorkModeTileUserActionInteractor +@Inject +constructor( + private val profileController: ManagedProfileController, + private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, +) : QSTileUserActionInteractor<WorkModeTileModel> { + override suspend fun handleInput(input: QSTileInput<WorkModeTileModel>) = + with(input) { + when (action) { + is QSTileUserAction.Click -> { + if (data is WorkModeTileModel.HasActiveProfile) { + profileController.setWorkModeEnabled(!data.isEnabled) + } + } + is QSTileUserAction.LongClick -> { + if (data is WorkModeTileModel.HasActiveProfile) { + qsTileIntentUserActionHandler.handle( + action.view, + Intent(Settings.ACTION_MANAGED_PROFILE_SETTINGS) + ) + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt new file mode 100644 index 000000000000..ae8382daf77d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.work.domain.model + +/** Work mode tile model. */ +sealed interface WorkModeTileModel { + /** @param isEnabled is true when the work mode is enabled */ + data class HasActiveProfile(val isEnabled: Boolean) : WorkModeTileModel + data object NoActiveProfile : WorkModeTileModel +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt new file mode 100644 index 000000000000..55445bb922a5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.work.ui + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_WORK_PROFILE_LABEL +import android.content.res.Resources +import android.service.quicksettings.Tile +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import javax.inject.Inject + +/** Maps [WorkModeTileModel] to [QSTileState]. */ +class WorkModeTileMapper +@Inject +constructor( + @Main private val resources: Resources, + private val theme: Resources.Theme, + private val devicePolicyManager: DevicePolicyManager, +) : QSTileDataToStateMapper<WorkModeTileModel> { + override fun map(config: QSTileConfig, data: WorkModeTileModel): QSTileState = + QSTileState.build(resources, theme, config.uiConfig) { + label = getTileLabel()!! + contentDescription = label + + icon = { + Icon.Loaded( + resources.getDrawable( + com.android.internal.R.drawable.stat_sys_managed_profile_status, + theme + ), + contentDescription = null + ) + } + + when (data) { + is WorkModeTileModel.HasActiveProfile -> { + if (data.isEnabled) { + activationState = QSTileState.ActivationState.ACTIVE + secondaryLabel = "" + } else { + activationState = QSTileState.ActivationState.INACTIVE + secondaryLabel = + resources.getString(R.string.quick_settings_work_mode_paused_state) + } + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + } + is WorkModeTileModel.NoActiveProfile -> { + activationState = QSTileState.ActivationState.UNAVAILABLE + secondaryLabel = + resources.getStringArray(R.array.tile_states_work)[Tile.STATE_UNAVAILABLE] + supportedActions = setOf() + } + } + + sideViewIcon = QSTileState.SideViewIcon.None + } + + private fun getTileLabel(): CharSequence? { + return devicePolicyManager.resources.getString(QS_WORK_PROFILE_LABEL) { + resources.getString(R.string.quick_settings_work_mode_label) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt index c1b20374dbac..671050477042 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt @@ -23,16 +23,21 @@ import android.view.View import androidx.annotation.VisibleForTesting import androidx.asynclayoutinflater.view.AsyncLayoutInflater import com.android.settingslib.applications.InterestingConfigChanges +import com.android.systemui.Dumpable import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.qs.QSContainerController import com.android.systemui.qs.QSContainerImpl import com.android.systemui.qs.QSImpl import com.android.systemui.qs.dagger.QSSceneComponent import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.util.kotlin.sample +import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider import kotlin.coroutines.resume @@ -107,11 +112,17 @@ interface QSSceneAdapter { } /** State for appearing QQS from Lockscreen or Gone */ - data class Unsquishing(override val squishiness: Float) : State { + data class UnsquishingQQS(override val squishiness: Float) : State { override val isVisible = true override val expansion = 0f } + /** State for appearing QS from Lockscreen or Gone, used in Split shade */ + data class UnsquishingQS(override val squishiness: Float) : State { + override val isVisible = true + override val expansion = 1f + } + companion object { // These are special cases of the expansion. val QQS = Expanding(0f) @@ -129,22 +140,28 @@ class QSSceneAdapterImpl constructor( private val qsSceneComponentFactory: QSSceneComponent.Factory, private val qsImplProvider: Provider<QSImpl>, + shadeInteractor: ShadeInteractor, + dumpManager: DumpManager, @Main private val mainDispatcher: CoroutineDispatcher, @Application applicationScope: CoroutineScope, private val configurationInteractor: ConfigurationInteractor, private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater, -) : QSContainerController, QSSceneAdapter { +) : QSContainerController, QSSceneAdapter, Dumpable { @Inject constructor( qsSceneComponentFactory: QSSceneComponent.Factory, qsImplProvider: Provider<QSImpl>, + shadeInteractor: ShadeInteractor, + dumpManager: DumpManager, @Main dispatcher: CoroutineDispatcher, @Application scope: CoroutineScope, configurationInteractor: ConfigurationInteractor, ) : this( qsSceneComponentFactory, qsImplProvider, + shadeInteractor, + dumpManager, dispatcher, scope, configurationInteractor, @@ -182,6 +199,7 @@ constructor( ) init { + dumpManager.registerDumpable(this) applicationScope.launch { launch { state.sample(_isCustomizing, ::Pair).collect { (state, customizing) -> @@ -210,6 +228,11 @@ constructor( it.second.applyBottomNavBarToCustomizerPadding(it.first) } } + launch { + shadeInteractor.shadeMode.collect { + qsImpl.value?.setInSplitShade(it == ShadeMode.Split) + } + } } } @@ -256,9 +279,17 @@ constructor( private fun QSImpl.applyState(state: QSSceneAdapter.State) { setQsVisible(state.isVisible) - setExpanded(state.isVisible) + setExpanded(state.isVisible && state.expansion > 0f) setListening(state.isVisible) setQsExpansion(state.expansion, 1f, 0f, state.squishiness) - setTransitionToFullShadeProgress(false, 1f, state.squishiness) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.apply { + println("Last state: ${state.value}") + println("Customizing: ${isCustomizing.value}") + println("QQS height: $qqsHeight") + println("QS height: $qsHeight") + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt index 34f66b85def1..c695d4c98308 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt @@ -48,6 +48,8 @@ constructor( qsSceneAdapter.isCustomizing.map { customizing -> if (customizing) { mapOf<UserAction, UserActionResult>(Back to UserActionResult(Scenes.QuickSettings)) + // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade + // while customizing } else { mapOf( Back to UserActionResult(Scenes.Shade), diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index d0ff33869a77..7c1a2c032bea 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -86,7 +86,6 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.WakefulnessLifecycle; @@ -146,7 +145,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000; private final Context mContext; - private final FeatureFlags mFeatureFlags; private final SceneContainerFlags mSceneContainerFlags; private final Executor mMainExecutor; private final ShellInterface mShellInterface; @@ -209,8 +207,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis @Override public void onStatusBarTouchEvent(MotionEvent event) { verifyCallerAndClearCallingIdentity("onStatusBarTouchEvent", () -> { - // TODO move this logic to message queue - if (event.getActionMasked() == ACTION_DOWN) { + if (mSceneContainerFlags.isEnabled()) { + //TODO(b/329863123) implement latency tracking for shade scene + Log.i(TAG_OPS, "Scene container enabled. Latency tracking not started."); + } else if (event.getActionMasked() == ACTION_DOWN) { mShadeViewControllerLazy.get().startExpandLatencyTracking(); } mHandler.post(() -> { @@ -600,7 +600,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis KeyguardUnlockAnimationController sysuiUnlockAnimationController, InWindowLauncherUnlockAnimationManager inWindowLauncherUnlockAnimationManager, AssistUtils assistUtils, - FeatureFlags featureFlags, SceneContainerFlags sceneContainerFlags, DumpManager dumpManager, Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder, @@ -613,7 +612,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } mContext = context; - mFeatureFlags = featureFlags; mSceneContainerFlags = sceneContainerFlags; mMainExecutor = mainExecutor; mShellInterface = shellInterface; diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt index 7009816942f2..5e4919d44f23 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt @@ -59,6 +59,7 @@ constructor( keyguardDismissUtil: KeyguardDismissUtil, private val dialogTransitionAnimator: DialogTransitionAnimator, private val panelInteractor: PanelInteractor, + private val issueRecordingState: IssueRecordingState, ) : RecordingService( controller, @@ -90,6 +91,7 @@ constructor( DEFAULT_MAX_TRACE_SIZE, DEFAULT_MAX_TRACE_DURATION_IN_MINUTES ) + issueRecordingState.isRecording = true if (!intent.getBooleanExtra(EXTRA_SCREEN_RECORD, false)) { // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action // will circumvent the RecordingService's screen recording start code. @@ -103,6 +105,7 @@ constructor( // this line should be removed. getSystemService(LauncherApps::class.java)?.saveViewCaptureData() TraceUtils.traceStop(contentResolver) + issueRecordingState.isRecording = false } ACTION_SHARE -> { shareRecording(intent) diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt new file mode 100644 index 000000000000..394c5c2775a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.recordissue + +import com.android.systemui.dagger.SysUISingleton +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject + +@SysUISingleton +class IssueRecordingState @Inject constructor() { + + private val listeners = CopyOnWriteArrayList<Runnable>() + + var isRecording = false + set(value) { + field = value + listeners.forEach(Runnable::run) + } + + fun addListener(listener: Runnable) { + listeners.add(listener) + } + + fun removeListener(listener: Runnable) { + listeners.remove(listener) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt index 7313a49be1bf..832fc3f00022 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt @@ -17,6 +17,7 @@ package com.android.systemui.recordissue import android.annotation.SuppressLint +import android.app.AlertDialog import android.content.Context import android.content.res.ColorStateList import android.graphics.Color @@ -74,7 +75,6 @@ constructor( @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch private lateinit var issueTypeButton: Button - private var hasSelectedIssueType: Boolean = false @MainThread override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { @@ -86,15 +86,13 @@ constructor( setPositiveButton( R.string.qs_record_issue_start, { _, _ -> - if (hasSelectedIssueType) { - onStarted.accept( - IssueRecordingConfig( - screenRecordSwitch.isChecked, - true /* TODO: Base this on issueType selected */ - ) + onStarted.accept( + IssueRecordingConfig( + screenRecordSwitch.isChecked, + true /* TODO: Base this on issueType selected */ ) - dismiss() - } + ) + dismiss() }, false ) @@ -115,8 +113,12 @@ constructor( bgExecutor.execute { onScreenRecordSwitchClicked() } } } + val startButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) issueTypeButton = requireViewById(R.id.issue_type_button) - issueTypeButton.setOnClickListener { onIssueTypeClicked(context) } + issueTypeButton.setOnClickListener { + onIssueTypeClicked(context) { startButton.isEnabled = true } + } + startButton.isEnabled = false } } @@ -159,7 +161,7 @@ constructor( } @MainThread - private fun onIssueTypeClicked(context: Context) { + private fun onIssueTypeClicked(context: Context, onIssueTypeSelected: Runnable) { val selectedCategory = issueTypeButton.text.toString() val popupMenu = PopupMenu(context, issueTypeButton) @@ -174,11 +176,11 @@ constructor( popupMenu.apply { setOnMenuItemClickListener { issueTypeButton.text = it.title + onIssueTypeSelected.run() true } setForceShowIcon(true) show() } - hasSelectedIssueType = true } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt index 467089d24f2c..54ec398cd9a7 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt @@ -18,18 +18,15 @@ package com.android.systemui.scene.shared.flag -import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR -import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR -import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.Flags.FLAG_SCENE_CONTAINER -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.keyguardWmStateRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.Flags.sceneContainer import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FlagToken import com.android.systemui.flags.Flags.SCENE_CONTAINER_ENABLED import com.android.systemui.flags.RefactorFlagUtils +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.KeyguardWmStateRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.media.controls.util.MediaInSceneContainerFlag import dagger.Module @@ -45,11 +42,11 @@ object SceneContainerFlag { get() = SCENE_CONTAINER_ENABLED && // mainStaticFlag sceneContainer() && // mainAconfigFlag - keyguardBottomAreaRefactor() && - migrateClocksToBlueprint() && + KeyguardBottomAreaRefactor.isEnabled && + MigrateClocksToBlueprint.isEnabled && ComposeLockscreen.isEnabled && MediaInSceneContainerFlag.isEnabled && - keyguardWmStateRefactor() + KeyguardWmStateRefactor.isEnabled // NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer /** @@ -66,9 +63,9 @@ object SceneContainerFlag { /** The set of secondary flags which must be enabled for scene container to work properly */ inline fun getSecondaryFlags(): Sequence<FlagToken> = sequenceOf( - FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()), - FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint()), - FlagToken(FLAG_KEYGUARD_WM_STATE_REFACTOR, keyguardWmStateRefactor()), + KeyguardBottomAreaRefactor.token, + MigrateClocksToBlueprint.token, + KeyguardWmStateRefactor.token, ComposeLockscreen.token, MediaInSceneContainerFlag.token, // NOTE: Changes should also be made in isEnabled and @EnableSceneContainer diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt new file mode 100644 index 000000000000..97acccde2524 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.UserHandle +import androidx.appcompat.content.res.AppCompatResources +import com.android.systemui.res.R +import javax.inject.Inject + +/** + * Provides actions for screenshots. This class can be overridden by a vendor-specific SysUI + * implementation. + */ +interface ScreenshotActionsProvider { + data class ScreenshotAction( + val icon: Drawable? = null, + val text: String? = null, + val description: String, + val overrideTransition: Boolean = false, + val retrieveIntent: (Uri) -> Intent + ) + + interface ScreenshotActionsCallback { + fun setPreviewAction(overrideTransition: Boolean = false, retrieveIntent: (Uri) -> Intent) + fun addAction(action: ScreenshotAction) = addActions(listOf(action)) + fun addActions(actions: List<ScreenshotAction>) + } + + interface Factory { + fun create( + context: Context, + user: UserHandle?, + callback: ScreenshotActionsCallback + ): ScreenshotActionsProvider + } +} + +class DefaultScreenshotActionsProvider( + private val context: Context, + private val user: UserHandle?, + private val callback: ScreenshotActionsProvider.ScreenshotActionsCallback +) : ScreenshotActionsProvider { + init { + callback.setPreviewAction(true) { ActionIntentCreator.createEdit(it, context) } + val editAction = + ScreenshotActionsProvider.ScreenshotAction( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit), + context.resources.getString(R.string.screenshot_edit_label), + context.resources.getString(R.string.screenshot_edit_description), + true + ) { uri -> + ActionIntentCreator.createEdit(uri, context) + } + val shareAction = + ScreenshotActionsProvider.ScreenshotAction( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share), + context.resources.getString(R.string.screenshot_share_label), + context.resources.getString(R.string.screenshot_share_description), + false + ) { uri -> + ActionIntentCreator.createShare(uri) + } + callback.addActions(listOf(editAction, shareAction)) + } + + class Factory @Inject constructor() : ScreenshotActionsProvider.Factory { + override fun create( + context: Context, + user: UserHandle?, + callback: ScreenshotActionsProvider.ScreenshotActionsCallback + ): ScreenshotActionsProvider { + return DefaultScreenshotActionsProvider(context, user, callback) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index c8e13bb8c2fc..b796a206b5b4 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -19,6 +19,7 @@ package com.android.systemui.screenshot; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; +import static com.android.systemui.Flags.screenshotShelfUi; import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; @@ -237,6 +238,7 @@ public class ScreenshotController { private final WindowContext mContext; private final FeatureFlags mFlags; private final ScreenshotViewProxy mViewProxy; + private final ScreenshotActionsProvider.Factory mActionsProviderFactory; private final ScreenshotNotificationsController mNotificationsController; private final ScreenshotSmartActions mScreenshotSmartActions; private final UiEventLogger mUiEventLogger; @@ -271,6 +273,8 @@ public class ScreenshotController { private boolean mScreenshotTakenInPortrait; private boolean mBlockAttach; + private ScreenshotActionsProvider mActionsProvider; + private Animator mScreenshotAnimation; private RequestCallback mCurrentRequestCallback; private String mPackageName = ""; @@ -298,6 +302,7 @@ public class ScreenshotController { Context context, FeatureFlags flags, ScreenshotViewProxy.Factory viewProxyFactory, + ScreenshotActionsProvider.Factory actionsProviderFactory, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory, ScrollCaptureClient scrollCaptureClient, @@ -349,6 +354,7 @@ public class ScreenshotController { mAssistContentRequester = assistContentRequester; mViewProxy = viewProxyFactory.getProxy(mContext, mDisplayId); + mActionsProviderFactory = actionsProviderFactory; mScreenshotHandler.setOnTimeoutRunnable(() -> { if (DEBUG_UI) { @@ -393,6 +399,7 @@ public class ScreenshotController { void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, RequestCallback requestCallback) { Assert.isMainThread(); + mCurrentRequestCallback = requestCallback; if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) { Rect bounds = getFullScreenRect(); @@ -496,7 +503,7 @@ public class ScreenshotController { return mDisplayId == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay; } - void prepareViewForNewScreenshot(ScreenshotData screenshot, String oldPackageName) { + void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) { withWindowAttached(() -> { if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) { mViewProxy.announceForAccessibility(mContext.getResources().getString( @@ -509,6 +516,11 @@ public class ScreenshotController { mViewProxy.reset(); + if (screenshotShelfUi()) { + mActionsProvider = mActionsProviderFactory.create(mContext, screenshot.getUserHandle(), + ((ScreenshotActionsProvider.ScreenshotActionsCallback) mViewProxy)); + } + if (mViewProxy.isAttachedToWindow()) { // if we didn't already dismiss for another reason if (!mViewProxy.isDismissing()) { @@ -983,20 +995,16 @@ public class ScreenshotController { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); - doPostAnimation(imageData); + mViewProxy.setChipIntents(imageData); } }); } else { - doPostAnimation(imageData); + mViewProxy.setChipIntents(imageData); } }); } } - private void doPostAnimation(ScreenshotController.SavedImageData imageData) { - mViewProxy.setChipIntents(imageData); - } - /** * Sets up the action shade and its entrance animation, once we get the Quick Share action data. */ diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt new file mode 100644 index 000000000000..88bca951beb6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.screenshot + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.Notification +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Rect +import android.net.Uri +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.ScrollCaptureResponse +import android.view.View +import android.view.ViewTreeObserver +import android.view.WindowInsets +import android.window.OnBackInvokedCallback +import android.window.OnBackInvokedDispatcher +import com.android.internal.logging.UiEventLogger +import com.android.systemui.log.DebugLogger.debugLog +import com.android.systemui.res.R +import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS +import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS +import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT +import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW +import com.android.systemui.screenshot.ScreenshotController.SavedImageData +import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER +import com.android.systemui.screenshot.scroll.ScrollCaptureController +import com.android.systemui.screenshot.ui.ScreenshotAnimationController +import com.android.systemui.screenshot.ui.ScreenshotShelfView +import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Controls the screenshot view and viewModel. */ +class ScreenshotShelfViewProxy +@AssistedInject +constructor( + private val logger: UiEventLogger, + private val viewModel: ScreenshotViewModel, + @Assisted private val context: Context, + @Assisted private val displayId: Int +) : ScreenshotViewProxy, ScreenshotActionsProvider.ScreenshotActionsCallback { + override val view: ScreenshotShelfView = + LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView + override val screenshotPreview: View + override var packageName: String = "" + override var callbacks: ScreenshotView.ScreenshotViewCallback? = null + override var screenshot: ScreenshotData? = null + set(value) { + viewModel.setScreenshotBitmap(value?.bitmap) + field = value + } + + override val isAttachedToWindow + get() = view.isAttachedToWindow + override var isDismissing = false + override var isPendingSharedTransition = false + + private val animationController = ScreenshotAnimationController(view) + private var imageData: SavedImageData? = null + private var runOnImageDataAcquired: ((SavedImageData) -> Unit)? = null + + init { + ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context)) + addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" } + screenshotPreview = view.screenshotPreview + } + + override fun reset() { + animationController.cancel() + isPendingSharedTransition = false + imageData = null + viewModel.reset() + runOnImageDataAcquired = null + } + override fun updateInsets(insets: WindowInsets) {} + override fun updateOrientation(insets: WindowInsets) {} + + override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator { + return animationController.getEntranceAnimation() + } + + override fun addQuickShareChip(quickShareAction: Notification.Action) {} + + override fun setChipIntents(data: SavedImageData) { + imageData = data + runOnImageDataAcquired?.invoke(data) + } + + override fun requestDismissal(event: ScreenshotEvent) { + debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" } + + // If we're already animating out, don't restart the animation + if (isDismissing) { + debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" } + return + } + logger.log(event, 0, packageName) + val animator = animationController.getExitAnimation() + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + isDismissing = true + } + override fun onAnimationEnd(animator: Animator) { + isDismissing = false + callbacks?.onDismiss() + } + } + ) + animator.start() + } + + override fun showScrollChip(packageName: String, onClick: Runnable) {} + + override fun hideScrollChip() {} + + override fun prepareScrollingTransition( + response: ScrollCaptureResponse, + screenBitmap: Bitmap, + newScreenshot: Bitmap, + screenshotTakenInPortrait: Boolean, + onTransitionPrepared: Runnable, + ) {} + + override fun startLongScreenshotTransition( + transitionDestination: Rect, + onTransitionEnd: Runnable, + longScreenshot: ScrollCaptureController.LongScreenshot + ) {} + + override fun restoreNonScrollingUi() {} + + override fun stopInputListening() {} + + override fun requestFocus() { + view.requestFocus() + } + + override fun announceForAccessibility(string: String) = view.announceForAccessibility(string) + + override fun prepareEntranceAnimation(runnable: Runnable) { + view.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + debugLog(DEBUG_WINDOW) { "onPreDraw: startAnimation" } + view.viewTreeObserver.removeOnPreDrawListener(this) + runnable.run() + return true + } + } + ) + } + + private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + val onBackInvokedCallback = OnBackInvokedCallback { + debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + } + view.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + debugLog(DEBUG_INPUT) { "Registering Predictive Back callback" } + view + .findOnBackInvokedDispatcher() + ?.registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + onBackInvokedCallback + ) + } + + override fun onViewDetachedFromWindow(view: View) { + debugLog(DEBUG_INPUT) { "Unregistering Predictive Back callback" } + view + .findOnBackInvokedDispatcher() + ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) + } + } + ) + } + private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + view.setOnKeyListener( + object : View.OnKeyListener { + override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { + debugLog(DEBUG_INPUT) { "onKeyEvent: $keyCode" } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + return true + } + return false + } + } + ) + } + + @AssistedFactory + interface Factory : ScreenshotViewProxy.Factory { + override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy + } + + override fun setPreviewAction(overrideTransition: Boolean, retrieveIntent: (Uri) -> Intent) { + viewModel.setPreviewAction { + imageData?.let { + val intent = retrieveIntent(it.uri) + debugLog(DEBUG_ACTIONS) { "Preview tapped: $intent" } + isPendingSharedTransition = true + callbacks?.onAction(intent, it.owner, overrideTransition) + } + } + } + + override fun addActions(actions: List<ScreenshotActionsProvider.ScreenshotAction>) { + viewModel.addActions( + actions.map { action -> + ActionButtonViewModel(action.icon, action.text, action.description) { + val actionRunnable = + getActionRunnable(action.retrieveIntent, action.overrideTransition) + imageData?.let { actionRunnable(it) } + ?: run { runOnImageDataAcquired = actionRunnable } + } + } + ) + } + + private fun getActionRunnable( + retrieveIntent: (Uri) -> Intent, + overrideTransition: Boolean + ): (SavedImageData) -> Unit { + val onClick: (SavedImageData) -> Unit = { + val intent = retrieveIntent(it.uri) + debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" } + isPendingSharedTransition = true + callbacks!!.onAction(intent, it.owner, overrideTransition) + } + return onClick + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java index 06c0b8b6e769..c89b47612814 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java @@ -33,6 +33,7 @@ import android.content.Intent; import android.content.Intent.CaptureContentForNoteStatusCodes; import android.content.res.Resources; import android.os.IBinder; +import android.util.Log; import androidx.annotation.Nullable; @@ -58,6 +59,8 @@ import javax.inject.Inject; */ public class AppClipsService extends Service { + private static final String TAG = AppClipsService.class.getSimpleName(); + @Application private final Context mContext; private final FeatureFlags mFeatureFlags; private final Optional<Bubbles> mOptionalBubbles; @@ -77,14 +80,22 @@ public class AppClipsService extends Service { private boolean checkIndependentVariables() { if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) { + Log.d(TAG, "Feature flag disabled"); return false; } if (mOptionalBubbles.isEmpty()) { + Log.d(TAG, "Bubbles not available"); return false; } - return isComponentValid(); + if (isComponentValid()) { + Log.d(TAG, "checkIndependentVariables returned true"); + return true; + } + + Log.d(TAG, "checkIndependentVariables returned false"); + return false; } private boolean isComponentValid() { @@ -93,12 +104,27 @@ public class AppClipsService extends Service { componentName = ComponentName.unflattenFromString( mContext.getString(R.string.config_screenshotAppClipsActivityComponent)); } catch (Resources.NotFoundException e) { + Log.d(TAG, "AppClips activity component resource not defined"); + return false; + } + + if (componentName == null) { + Log.d(TAG, "AppClips component name not defined"); + return false; + } + + if (componentName.getPackageName().isEmpty()) { + Log.d(TAG, "AppClips component package name is empty"); + return false; + } + + if (componentName.getClassName().isEmpty()) { + Log.d(TAG, "AppClips component class name is empty"); return false; } - return componentName != null - && !componentName.getPackageName().isEmpty() - && !componentName.getClassName().isEmpty(); + Log.d(TAG, "isComponentValid returned true"); + return true; } @Nullable @@ -107,24 +133,39 @@ public class AppClipsService extends Service { return new IAppClipsService.Stub() { @Override public boolean canLaunchCaptureContentActivityForNote(int taskId) { - return canLaunchCaptureContentActivityForNoteInternal(taskId) - == CAPTURE_CONTENT_FOR_NOTE_SUCCESS; + if (canLaunchCaptureContentActivityForNoteInternal(taskId) + == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) { + Log.d(TAG, String.format("Can launch AppClips returned true for %d", taskId)); + return true; + } + + Log.d(TAG, String.format("Can launch AppClips returned false for %d", taskId)); + return false; } @Override @CaptureContentForNoteStatusCodes public int canLaunchCaptureContentActivityForNoteInternal(int taskId) { if (!mAreTaskAndTimeIndependentPrerequisitesMet) { + Log.d(TAG, + String.format("Task (%d) and time independent prereqs not met", taskId)); return CAPTURE_CONTENT_FOR_NOTE_FAILED; } if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) { + Log.d(TAG, String.format("Taskid %d is not app bubble task", taskId)); return CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; } - return mDevicePolicyManager.getScreenCaptureDisabled(null) - ? CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN - : CAPTURE_CONTENT_FOR_NOTE_SUCCESS; + if (mDevicePolicyManager.getScreenCaptureDisabled(null)) { + Log.d(TAG, + String.format("Screen capture disabled by admin, taskId %d", taskId)); + return CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN; + } + + Log.d(TAG, + String.format("Can launch AppClips (internal) successful for %d", taskId)); + return CAPTURE_CONTENT_FOR_NOTE_SUCCESS; } }; } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index cdb9abb15e84..2ce6d8380e36 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -16,16 +16,23 @@ package com.android.systemui.screenshot.dagger; +import static com.android.systemui.Flags.screenshotShelfUi; + import android.app.Service; +import android.view.accessibility.AccessibilityManager; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.screenshot.DefaultScreenshotActionsProvider; import com.android.systemui.screenshot.ImageCapture; import com.android.systemui.screenshot.ImageCaptureImpl; import com.android.systemui.screenshot.LegacyScreenshotViewProxy; import com.android.systemui.screenshot.RequestProcessor; +import com.android.systemui.screenshot.ScreenshotActionsProvider; import com.android.systemui.screenshot.ScreenshotPolicy; import com.android.systemui.screenshot.ScreenshotPolicyImpl; import com.android.systemui.screenshot.ScreenshotProxyService; import com.android.systemui.screenshot.ScreenshotRequestProcessor; +import com.android.systemui.screenshot.ScreenshotShelfViewProxy; import com.android.systemui.screenshot.ScreenshotSoundController; import com.android.systemui.screenshot.ScreenshotSoundControllerImpl; import com.android.systemui.screenshot.ScreenshotSoundProvider; @@ -34,6 +41,7 @@ import com.android.systemui.screenshot.ScreenshotViewProxy; import com.android.systemui.screenshot.TakeScreenshotService; import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService; import com.android.systemui.screenshot.appclips.AppClipsService; +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel; import dagger.Binds; import dagger.Module; @@ -85,9 +93,25 @@ public abstract class ScreenshotModule { abstract ScreenshotSoundController bindScreenshotSoundController( ScreenshotSoundControllerImpl screenshotSoundProviderImpl); + @Binds + abstract ScreenshotActionsProvider.Factory bindScreenshotActionsProviderFactory( + DefaultScreenshotActionsProvider.Factory defaultScreenshotActionsProviderFactory); + + @Provides + @SysUISingleton + static ScreenshotViewModel providesScreenshotViewModel( + AccessibilityManager accessibilityManager) { + return new ScreenshotViewModel(accessibilityManager); + } + @Provides static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory( + ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory, LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) { - return legacyScreenshotViewProxyFactory; + if (screenshotShelfUi()) { + return shelfScreenshotViewProxyFactory; + } else { + return legacyScreenshotViewProxyFactory; + } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt new file mode 100644 index 000000000000..2c178736d9c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.view.View + +class ScreenshotAnimationController(private val view: View) { + private var animator: Animator? = null + + fun getEntranceAnimation(): Animator { + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.addUpdateListener { view.alpha = it.animatedFraction } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + view.alpha = 0f + } + override fun onAnimationEnd(animator: Animator) { + view.alpha = 1f + } + } + ) + this.animator = animator + return animator + } + + fun getExitAnimation(): Animator { + val animator = ValueAnimator.ofFloat(1f, 0f) + animator.addUpdateListener { view.alpha = it.animatedValue as Float } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + view.alpha = 1f + } + override fun onAnimationEnd(animator: Animator) { + view.alpha = 0f + } + } + ) + this.animator = animator + return animator + } + + fun cancel() { + animator?.cancel() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt new file mode 100644 index 000000000000..747ad4f9e48c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.screenshot.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.systemui.res.R + +class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) : + ConstraintLayout(context, attrs) { + lateinit var screenshotPreview: ImageView + + override fun onFinishInflate() { + super.onFinishInflate() + screenshotPreview = requireViewById(R.id.screenshot_preview) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt new file mode 100644 index 000000000000..c7fe3f608a2f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.binder + +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel + +object ActionButtonViewBinder { + /** Binds the given view to the given view-model */ + fun bind(view: View, viewModel: ActionButtonViewModel) { + val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon) + val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text) + iconView.setImageDrawable(viewModel.icon) + textView.text = viewModel.name + setMargins(iconView, textView, viewModel.name?.isNotEmpty() ?: false) + if (viewModel.onClicked != null) { + view.setOnClickListener { viewModel.onClicked.invoke() } + } else { + view.setOnClickListener(null) + } + view.contentDescription = viewModel.description + view.visibility = View.VISIBLE + view.alpha = 1f + } + + private fun setMargins(iconView: View, textView: View, hasText: Boolean) { + val iconParams = iconView.layoutParams as LinearLayout.LayoutParams + val textParams = textView.layoutParams as LinearLayout.LayoutParams + if (hasText) { + iconParams.marginStart = iconView.dpToPx(R.dimen.overlay_action_chip_padding_start) + iconParams.marginEnd = iconView.dpToPx(R.dimen.overlay_action_chip_spacing) + textParams.marginStart = 0 + textParams.marginEnd = textView.dpToPx(R.dimen.overlay_action_chip_padding_end) + } else { + val paddingHorizontal = + iconView.dpToPx(R.dimen.overlay_action_chip_icon_only_padding_horizontal) + iconParams.marginStart = paddingHorizontal + iconParams.marginEnd = paddingHorizontal + } + iconView.layoutParams = iconParams + textView.layoutParams = textParams + } + + private fun View.dpToPx(dimenId: Int): Int { + return this.resources.getDimensionPixelSize(dimenId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt new file mode 100644 index 000000000000..d8782009e24b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt @@ -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 com.android.systemui.screenshot.ui.binder + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import com.android.systemui.util.children +import kotlinx.coroutines.launch + +object ScreenshotShelfViewBinder { + fun bind( + view: ViewGroup, + viewModel: ScreenshotViewModel, + layoutInflater: LayoutInflater, + ) { + val previewView: ImageView = view.requireViewById(R.id.screenshot_preview) + val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border) + previewView.clipToOutline = true + val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions) + view.requireViewById<View>(R.id.screenshot_dismiss_button).visibility = + if (viewModel.showDismissButton) View.VISIBLE else View.GONE + + view.repeatWhenAttached { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.preview.collect { bitmap -> + if (bitmap != null) { + previewView.setImageBitmap(bitmap) + previewView.visibility = View.VISIBLE + previewBorder.visibility = View.VISIBLE + } else { + previewView.visibility = View.GONE + previewBorder.visibility = View.GONE + } + } + } + launch { + viewModel.previewAction.collect { onClick -> + previewView.setOnClickListener { onClick?.run() } + } + } + launch { + viewModel.actions.collect { actions -> + if (actions.isNotEmpty()) { + view + .requireViewById<View>(R.id.actions_container_background) + .visibility = View.VISIBLE + } + val viewPool = actionsContainer.children.toList() + actionsContainer.removeAllViews() + val actionButtons = + List(actions.size) { + viewPool.getOrElse(it) { + layoutInflater.inflate( + R.layout.overlay_action_chip, + actionsContainer, + false + ) + } + } + actionButtons.zip(actions).forEach { + actionsContainer.addView(it.first) + ActionButtonViewBinder.bind(it.first, it.second) + } + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt new file mode 100644 index 000000000000..05bfed159527 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.viewmodel + +import android.graphics.drawable.Drawable + +data class ActionButtonViewModel( + val icon: Drawable?, + val name: CharSequence?, + val description: CharSequence, + val onClicked: (() -> Unit)? +) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt new file mode 100644 index 000000000000..dc61d1e9c37b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot.ui.viewmodel + +import android.graphics.Bitmap +import android.view.accessibility.AccessibilityManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) { + private val _preview = MutableStateFlow<Bitmap?>(null) + val preview: StateFlow<Bitmap?> = _preview + private val _previewAction = MutableStateFlow<Runnable?>(null) + val previewAction: StateFlow<Runnable?> = _previewAction + private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>()) + val actions: StateFlow<List<ActionButtonViewModel>> = _actions + val showDismissButton: Boolean + get() = accessibilityManager.isEnabled + + fun setScreenshotBitmap(bitmap: Bitmap?) { + _preview.value = bitmap + } + + fun setPreviewAction(runnable: Runnable) { + _previewAction.value = runnable + } + + fun addActions(actions: List<ActionButtonViewModel>) { + val actionList = _actions.value.toMutableList() + actionList.addAll(actions) + _actions.value = actionList + } + + fun reset() { + _preview.value = null + _previewAction.value = null + _actions.value = listOf() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 3a2a081663cb..f928ccc46cd3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -24,8 +24,6 @@ import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE; import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; -import static com.android.systemui.Flags.keyguardBottomAreaRefactor; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.Flags.predictiveBackAnimateShade; import static com.android.systemui.Flags.smartspaceRelocateToBottom; import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; @@ -129,8 +127,10 @@ import com.android.systemui.dump.DumpsysTableLogger; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.fragments.FragmentService; +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewConfigurator; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; @@ -193,6 +193,7 @@ import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefac import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; @@ -1018,7 +1019,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump instantCollapse(); } else { mView.animate().cancel(); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mView.animate() .alpha(0f) .setStartDelay(0) @@ -1075,7 +1076,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mQsController.init(); mShadeHeadsUpTracker.addTrackingHeadsUpListener( mNotificationStackScrollLayoutController::setTrackingHeadsUp); - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled()) { setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area)); } @@ -1154,7 +1155,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Occluded->Lockscreen collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(), mOccludedToLockscreenTransition, mMainDispatcher); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(), setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); collectFlow(mView, @@ -1165,7 +1166,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Lockscreen->Dreaming collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(), mLockscreenToDreamingTransition, mMainDispatcher); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(), setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); @@ -1177,7 +1178,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Gone->Dreaming collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(), mGoneToDreamingTransition, mMainDispatcher); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(), setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); } @@ -1188,7 +1189,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Lockscreen->Occluded collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(), mLockscreenToOccludedTransition, mMainDispatcher); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenAlpha(), setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenTranslationY(), @@ -1196,7 +1197,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } // Primary bouncer->Gone (ensures lockscreen content is not visible on successful auth) - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mPrimaryBouncerToGoneTransitionViewModel.getLockscreenAlpha(), setTransitionAlpha(mNotificationStackScrollLayoutController, /* excludeNotifications=*/ true), mMainDispatcher); @@ -1280,7 +1281,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStatusViewController.onDestroy(); } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { // Need a shared controller until mKeyguardStatusViewController can be removed from // here, due to important state being set in that controller. Rebind in order to pick // up config changes @@ -1332,13 +1333,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void onSplitShadeEnabledChanged() { mShadeLog.logSplitShadeChanged(mSplitShadeEnabled); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled); } // Reset any left over overscroll state. It is a rare corner case but can happen. mQsController.setOverScrollAmount(0); mScrimController.setNotificationsOverScrollAmount(0); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mNotificationStackScrollLayoutController.setOverExpansion(0); mNotificationStackScrollLayoutController.setOverScrollAmount(0); } @@ -1359,7 +1360,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } updateClockAppearance(); mQsController.updateQsState(); - if (!migrateClocksToBlueprint() && !FooterViewRefactor.isEnabled()) { + if (!MigrateClocksToBlueprint.isEnabled() && !FooterViewRefactor.isEnabled()) { mNotificationStackScrollLayoutController.updateFooter(); } } @@ -1391,7 +1392,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump void reInflateViews() { debugLog("reInflateViews"); // Re-inflate the status view group. - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { KeyguardStatusView keyguardStatusView = mNotificationContainerParent.findViewById(R.id.keyguard_status_view); int statusIndex = mNotificationContainerParent.indexOfChild(keyguardStatusView); @@ -1430,7 +1431,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump updateViewControllers(userAvatarView, keyguardUserSwitcherView); - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled()) { // Update keyguard bottom area int index = mView.indexOfChild(mKeyguardBottomArea); mView.removeView(mKeyguardBottomArea); @@ -1449,7 +1450,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(), mStatusBarStateController.getInterpolatedDozeAmount()); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.setKeyguardStatusViewVisibility( mBarState, false, @@ -1471,7 +1472,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mBarState); } - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled()) { setKeyguardBottomAreaVisibility(mBarState, false); } @@ -1480,14 +1481,14 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void attachSplitShadeMediaPlayerContainer(FrameLayout container) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } mKeyguardMediaController.attachSplitShadeContainer(container); } private void initBottomArea() { - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled()) { mKeyguardBottomArea.init( mKeyguardBottomAreaViewModel, mFalsingManager, @@ -1513,7 +1514,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void updateMaxDisplayedNotifications(boolean recompute) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } @@ -1630,7 +1631,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump int userSwitcherPreferredY = mStatusBarHeaderHeightKeyguard; boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled(); boolean shouldAnimateClockChange = mScreenOffAnimationController.shouldAnimateClockChange(); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mKeyguardClockInteractor.setClockSize(computeDesiredClockSize()); } else { mKeyguardStatusViewController.displayClock(computeDesiredClockSize(), @@ -1671,11 +1672,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStatusViewController.getClockBottom(mStatusBarHeaderHeightKeyguard), mKeyguardStatusViewController.isClockTopAligned()); mClockPositionAlgorithm.run(mClockPositionResult); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.setLockscreenClockY( mClockPositionAlgorithm.getExpandedPreferredClockY()); } - if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) { + if (!(MigrateClocksToBlueprint.isEnabled() || KeyguardBottomAreaRefactor.isEnabled())) { mKeyguardBottomAreaInteractor.setClockPosition( mClockPositionResult.clockX, mClockPositionResult.clockY); } @@ -1683,7 +1684,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange; - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.updatePosition( mClockPositionResult.clockX, mClockPositionResult.clockY, mClockPositionResult.clockScale, animateClock); @@ -1740,7 +1741,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // To prevent the weather clock from overlapping with the notification shelf on AOD, we use // the small clock here // With migrateClocksToBlueprint, weather clock will have behaviors similar to other clocks - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { if (mKeyguardStatusViewController.isLargeClockBlockingNotificationShelf() && hasVisibleNotifications() && isOnAod()) { return SMALL; @@ -1758,7 +1759,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void updateKeyguardStatusViewAlignment(boolean animate) { boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered(); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered); return; } @@ -1941,7 +1942,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha; mKeyguardStatusViewController.setAlpha(alpha); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { // TODO (b/296373478) This is for split shade media movement. } else { mKeyguardStatusViewController @@ -2498,7 +2499,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } if (!mKeyguardBypassController.getBypassEnabled()) { - if (migrateClocksToBlueprint() && !mSplitShadeEnabled) { + if (MigrateClocksToBlueprint.isEnabled() && !mSplitShadeEnabled) { return (int) mKeyguardInteractor.getNotificationContainerBounds() .getValue().getTop(); } @@ -2531,7 +2532,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump void requestScrollerTopPaddingUpdate(boolean animate) { float padding = mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing, getKeyguardNotificationStaticPadding(), mExpandedFraction); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mSharedNotificationContainerInteractor.setTopPosition(padding); } else { mNotificationStackScrollLayoutController.updateTopPadding(padding, animate); @@ -2712,7 +2713,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return; } - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { float alpha = 1f; if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp && !mHeadsUpManager.hasPinnedHeadsUp()) { @@ -2748,7 +2749,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void updateKeyguardBottomAreaAlpha() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } if (mIsOcclusionTransitionRunning) { @@ -2766,7 +2767,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump float alpha = Math.min(expansionAlpha, 1 - mQsController.computeExpansionFraction()); alpha *= mBottomAreaShadeAlpha; - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled()) { mKeyguardInteractor.setAlpha(alpha); } else { mKeyguardBottomAreaInteractor.setAlpha(alpha); @@ -2978,7 +2979,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void updateDozingVisibilities(boolean animate) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled()) { mKeyguardInteractor.setAnimateDozingTransitions(animate); } else { mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate); @@ -2990,7 +2991,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void onScreenTurningOn() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.dozeTimeTick(); } } @@ -3189,7 +3190,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mDozing = dozing; // TODO (b/) make listeners for this mNotificationStackScrollLayoutController.setDozing(mDozing, animate); - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled()) { mKeyguardInteractor.setAnimateDozingTransitions(animate); } else { mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate); @@ -3245,7 +3246,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump public void dozeTimeTick() { mLockIconViewController.dozeTimeTick(); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.dozeTimeTick(); } if (mInterpolatedDarkAmount > 0) { @@ -3257,7 +3258,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStatusViewController.setStatusAccessibilityImportance(mode); } - @Override public void performHapticFeedback(int constant) { mVibratorHelper.performHapticFeedback(mView, constant); } @@ -3325,7 +3325,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump /** Updates the views to the initial state for the fold to AOD animation. */ @Override public void prepareFoldToAodAnimation() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } // Force show AOD UI even if we are not locked @@ -3349,7 +3349,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void startFoldToAodAnimation(Runnable startAction, Runnable endAction, Runnable cancelAction) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } final ViewPropertyAnimator viewAnimator = mView.animate(); @@ -3387,7 +3387,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump /** Cancels fold to AOD transition and resets view state. */ @Override public void cancelFoldToAodAnimation() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } cancelAnimation(); @@ -4382,6 +4382,10 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void onHeadsUpPinned(NotificationEntry entry) { + if (NotificationsHeadsUpRefactor.isEnabled()) { + return; + } + if (!isKeyguardShowing()) { mNotificationStackScrollLayoutController.generateHeadsUpAnimation(entry, true); } @@ -4389,6 +4393,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void onHeadsUpUnPinned(NotificationEntry entry) { + if (NotificationsHeadsUpRefactor.isEnabled()) { + return; + } // When we're unpinning the notification via active edge they remain heads-upped, // we need to make sure that an animation happens in this case, otherwise the @@ -4461,7 +4468,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump && statusBarState == KEYGUARD) { // This means we're doing the screen off animation - position the keyguard status // view where it'll be on AOD, so we can animate it in. - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.updatePosition( mClockPositionResult.clockX, mClockPositionResult.clockYFullyDozing, @@ -4470,7 +4477,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } } - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.setKeyguardStatusViewVisibility( statusBarState, keyguardFadingAway, @@ -4478,7 +4485,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mBarState); } - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled()) { setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade); } @@ -4583,7 +4590,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump setDozing(true /* dozing */, false /* animate */); mStatusBarStateController.setUpcomingState(KEYGUARD); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mStatusBarStateController.setState(KEYGUARD); } else { mStatusBarStateListener.onStateChanged(KEYGUARD); @@ -4646,7 +4653,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth()); // Update Clock Pivot (used by anti-burnin transformations) - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.updatePivot(mView.getWidth(), mView.getHeight()); } @@ -4747,7 +4754,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump stackScroller.setMaxAlphaForKeyguard(alpha, "NPVC.setTransitionAlpha()"); } - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled()) { mKeyguardInteractor.setAlpha(alpha); } else { mKeyguardBottomAreaInteractor.setAlpha(alpha); @@ -4766,7 +4773,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private Consumer<Float> setTransitionY( NotificationStackScrollLayoutController stackScroller) { return (Float translationY) -> { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.setTranslationY(translationY, /* excludeMedia= */false); stackScroller.setTranslationY(translationY); @@ -4808,7 +4815,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump */ @Override public boolean onInterceptTouchEvent(MotionEvent event) { - if (migrateClocksToBlueprint() && !mUseExternalTouch) { + if (MigrateClocksToBlueprint.isEnabled() && !mUseExternalTouch) { return false; } @@ -4879,7 +4886,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mCentralSurfaces.userActivity(); } mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation; @@ -4980,7 +4987,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump */ @Override public boolean onTouchEvent(MotionEvent event) { - if (migrateClocksToBlueprint() && !mUseExternalTouch) { + if (MigrateClocksToBlueprint.isEnabled() && !mUseExternalTouch) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index e5771785409f..e8e629ca907d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -16,7 +16,6 @@ package com.android.systemui.shade; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; @@ -48,6 +47,7 @@ import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.flags.Flags; import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; @@ -320,7 +320,7 @@ public class NotificationShadeWindowViewController implements Dumpable { mTouchActive = true; mTouchCancelled = false; mDownEvent = ev; - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mService.userActivity(); } } else if (ev.getActionMasked() == MotionEvent.ACTION_UP @@ -475,7 +475,7 @@ public class NotificationShadeWindowViewController implements Dumpable { && !bouncerShowing && !mStatusBarStateController.isDozing()) { if (mDragDownHelper.isDragDownEnabled()) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { // When on lockscreen, if the touch originates at the top of the screen // go directly to QS and not the shade if (mStatusBarStateController.getState() == KEYGUARD @@ -488,7 +488,7 @@ public class NotificationShadeWindowViewController implements Dumpable { // This handles drag down over lockscreen boolean result = mDragDownHelper.onInterceptTouchEvent(ev); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { if (result) { mLastInterceptWasDragDownHelper = true; if (ev.getAction() == MotionEvent.ACTION_DOWN) { @@ -511,7 +511,7 @@ public class NotificationShadeWindowViewController implements Dumpable { return true; } } - } else if (migrateClocksToBlueprint()) { + } else if (MigrateClocksToBlueprint.isEnabled()) { // This final check handles swipes on HUNs and when Pulsing if (!bouncerShowing && didNotificationPanelInterceptEvent(ev)) { mShadeLogger.d("NSWVC: intercepted for HUN/PULSING"); @@ -526,7 +526,7 @@ public class NotificationShadeWindowViewController implements Dumpable { MotionEvent cancellation = MotionEvent.obtain(ev); cancellation.setAction(MotionEvent.ACTION_CANCEL); mStackScrollLayout.onInterceptTouchEvent(cancellation); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mNotificationPanelViewController.handleExternalInterceptTouch(cancellation); } cancellation.recycle(); @@ -541,7 +541,7 @@ public class NotificationShadeWindowViewController implements Dumpable { if (mStatusBarKeyguardViewManager.onTouch(ev)) { return true; } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { if (mLastInterceptWasDragDownHelper && (mDragDownHelper.isDraggingDown())) { // we still want to finish our drag down gesture when locking the screen handled |= mDragDownHelper.onTouchEvent(ev) || handled; @@ -631,7 +631,7 @@ public class NotificationShadeWindowViewController implements Dumpable { } private boolean didNotificationPanelInterceptEvent(MotionEvent ev) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { // Since NotificationStackScrollLayout is now a sibling of notification_panel, we need // to also ask NotificationPanelViewController directly, in order to process swipe up // events originating from notifications diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index 29de688fa7bf..8b88da1754f0 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -28,10 +28,10 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.lifecycle.lifecycleScope import com.android.systemui.Flags.centralizedStatusBarHeightFix -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.fragments.FragmentService +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.plugins.qs.QS @@ -52,11 +52,12 @@ import javax.inject.Inject import kotlin.reflect.KMutableProperty0 import kotlinx.coroutines.launch -@VisibleForTesting -internal const val INSET_DEBOUNCE_MILLIS = 500L +@VisibleForTesting internal const val INSET_DEBOUNCE_MILLIS = 500L @SysUISingleton -class NotificationsQSContainerController @Inject constructor( +class NotificationsQSContainerController +@Inject +constructor( view: NotificationsQuickSettingsContainer, private val navigationModeController: NavigationModeController, private val overviewProxyService: OverviewProxyService, @@ -64,8 +65,7 @@ class NotificationsQSContainerController @Inject constructor( private val shadeInteractor: ShadeInteractor, private val fragmentService: FragmentService, @Main private val delayableExecutor: DelayableExecutor, - private val - notificationStackScrollLayoutController: NotificationStackScrollLayoutController, + private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController, private val splitShadeStateController: SplitShadeStateController, private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController { @@ -88,45 +88,48 @@ class NotificationsQSContainerController @Inject constructor( private var isGestureNavigation = true private var taskbarVisible = false - private val taskbarVisibilityListener: OverviewProxyListener = object : OverviewProxyListener { - override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) { - taskbarVisible = visible + private val taskbarVisibilityListener: OverviewProxyListener = + object : OverviewProxyListener { + override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) { + taskbarVisible = visible + } } - } // With certain configuration changes (like light/dark changes), the nav bar will disappear // for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value // for 500ms. // All interactions with this object happen in the main thread. - private val delayedInsetSetter = object : Runnable, Consumer<WindowInsets> { - private var canceller: Runnable? = null - private var stableInsets = 0 - private var cutoutInsets = 0 - - override fun accept(insets: WindowInsets) { - // when taskbar is visible, stableInsetBottom will include its height - stableInsets = insets.stableInsetBottom - cutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0 - canceller?.run() - canceller = delayableExecutor.executeDelayed(this, INSET_DEBOUNCE_MILLIS) - } + private val delayedInsetSetter = + object : Runnable, Consumer<WindowInsets> { + private var canceller: Runnable? = null + private var stableInsets = 0 + private var cutoutInsets = 0 + + override fun accept(insets: WindowInsets) { + // when taskbar is visible, stableInsetBottom will include its height + stableInsets = insets.stableInsetBottom + cutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0 + canceller?.run() + canceller = delayableExecutor.executeDelayed(this, INSET_DEBOUNCE_MILLIS) + } - override fun run() { - bottomStableInsets = stableInsets - bottomCutoutInsets = cutoutInsets - updateBottomSpacing() + override fun run() { + bottomStableInsets = stableInsets + bottomCutoutInsets = cutoutInsets + updateBottomSpacing() + } } - } override fun onInit() { mView.repeatWhenAttached { lifecycleScope.launch { - shadeInteractor.isQsExpanded.collect{ _ -> mView.invalidate() } + shadeInteractor.isQsExpanded.collect { _ -> mView.invalidate() } } } - val currentMode: Int = navigationModeController.addListener { mode: Int -> - isGestureNavigation = QuickStepContract.isGesturalMode(mode) - } + val currentMode: Int = + navigationModeController.addListener { mode: Int -> + isGestureNavigation = QuickStepContract.isGesturalMode(mode) + } isGestureNavigation = QuickStepContract.isGesturalMode(currentMode) mView.setStackScroller(notificationStackScrollLayoutController.getView()) @@ -151,30 +154,35 @@ class NotificationsQSContainerController @Inject constructor( fun updateResources() { val newSplitShadeEnabled = - splitShadeStateController.shouldUseSplitNotificationShade(resources) + splitShadeStateController.shouldUseSplitNotificationShade(resources) val splitShadeEnabledChanged = newSplitShadeEnabled != splitShadeEnabled splitShadeEnabled = newSplitShadeEnabled largeScreenShadeHeaderActive = LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources) - notificationsBottomMargin = resources.getDimensionPixelSize( - R.dimen.notification_panel_margin_bottom) + notificationsBottomMargin = + resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom) largeScreenShadeHeaderHeight = calculateLargeShadeHeaderHeight() shadeHeaderHeight = calculateShadeHeaderHeight() - panelMarginHorizontal = resources.getDimensionPixelSize( - R.dimen.notification_panel_margin_horizontal) - topMargin = if (largeScreenShadeHeaderActive) { - largeScreenShadeHeaderHeight - } else { - resources.getDimensionPixelSize(R.dimen.notification_panel_margin_top) - } + panelMarginHorizontal = + resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) + topMargin = + if (largeScreenShadeHeaderActive) { + largeScreenShadeHeaderHeight + } else { + resources.getDimensionPixelSize(R.dimen.notification_panel_margin_top) + } updateConstraints() - val scrimMarginChanged = ::scrimShadeBottomMargin.setAndReportChange( - resources.getDimensionPixelSize(R.dimen.split_shade_notifications_scrim_margin_bottom) - ) - val footerOffsetChanged = ::footerActionsOffset.setAndReportChange( - resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) + - resources.getDimensionPixelSize(R.dimen.qs_footer_actions_bottom_padding) - ) + val scrimMarginChanged = + ::scrimShadeBottomMargin.setAndReportChange( + resources.getDimensionPixelSize( + R.dimen.split_shade_notifications_scrim_margin_bottom + ) + ) + val footerOffsetChanged = + ::footerActionsOffset.setAndReportChange( + resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) + + resources.getDimensionPixelSize(R.dimen.qs_footer_actions_bottom_padding) + ) val dimensChanged = scrimMarginChanged || footerOffsetChanged if (splitShadeEnabledChanged || dimensChanged) { @@ -198,7 +206,7 @@ class NotificationsQSContainerController @Inject constructor( // 2. carrier_group height (R.dimen.large_screen_shade_header_min_height) // 3. date height (R.dimen.new_qs_header_non_clickable_element_height) val estimatedHeight = - 2 * resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) + + 2 * resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) + resources.getDimensionPixelSize(R.dimen.new_qs_header_non_clickable_element_height) return estimatedHeight.coerceAtLeast(minHeight) } @@ -250,16 +258,17 @@ class NotificationsQSContainerController @Inject constructor( containerPadding = 0 stackScrollMargin = bottomStableInsets + notificationsBottomMargin } - val qsContainerPadding = if (!isQSDetailShowing) { - // We also want this padding in the bottom in these cases - if (splitShadeEnabled) { - stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset + val qsContainerPadding = + if (!isQSDetailShowing) { + // We also want this padding in the bottom in these cases + if (splitShadeEnabled) { + stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset + } else { + bottomStableInsets + } } else { - bottomStableInsets + 0 } - } else { - 0 - } return Paddings(containerPadding, stackScrollMargin, qsContainerPadding) } @@ -284,7 +293,7 @@ class NotificationsQSContainerController @Inject constructor( } private fun setNotificationsConstraints(constraintSet: ConstraintSet) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { return } val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID @@ -309,8 +318,8 @@ class NotificationsQSContainerController @Inject constructor( } private fun setKeyguardStatusViewConstraints(constraintSet: ConstraintSet) { - val statusViewMarginHorizontal = resources.getDimensionPixelSize( - R.dimen.status_view_margin_horizontal) + val statusViewMarginHorizontal = + resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) constraintSet.apply { setMargin(R.id.keyguard_status_view, START, statusViewMarginHorizontal) setMargin(R.id.keyguard_status_view, END, statusViewMarginHorizontal) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java index e82f2d3cbd30..13330553b2de 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java @@ -18,8 +18,6 @@ package com.android.systemui.shade; import static androidx.constraintlayout.core.widgets.Optimizer.OPTIMIZATION_GRAPH; -import static com.android.systemui.Flags.migrateClocksToBlueprint; - import android.app.Fragment; import android.content.Context; import android.content.res.Configuration; @@ -35,6 +33,7 @@ import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.plugins.qs.QS; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.AboveShelfObserver; @@ -190,7 +189,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return super.drawChild(canvas, child, drawingTime); } int layoutIndex = mLayoutDrawingOrder.indexOf(child); diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 8ba0544d5b7a..3a0e1678ff40 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -21,7 +21,6 @@ import static android.view.WindowInsets.Type.ime; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE; import static com.android.systemui.Flags.centralizedStatusBarHeightFix; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.shade.NotificationPanelViewController.COUNTER_PANEL_OPEN_QS; import static com.android.systemui.shade.NotificationPanelViewController.FLING_COLLAPSE; @@ -71,6 +70,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.fragments.FragmentHostManager; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; @@ -1280,18 +1280,20 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mScrimController.setScrimCornerRadius(radius); - // Convert global clipping coordinates to local ones, - // relative to NotificationStackScrollLayout - int nsslLeft = calculateNsslLeft(left); - int nsslRight = calculateNsslRight(right); - int nsslTop = getNotificationsClippingTopBounds(top); - int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop(); - int bottomRadius = mSplitShadeEnabled ? radius : 0; - // TODO (b/265193930): remove dependency on NPVC - int topRadius = mSplitShadeEnabled - && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius; - mNotificationStackScrollLayoutController.setRoundedClippingBounds( - nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius); + if (!SceneContainerFlag.isEnabled()) { + // Convert global clipping coordinates to local ones, + // relative to NotificationStackScrollLayout + int nsslLeft = calculateNsslLeft(left); + int nsslRight = calculateNsslRight(right); + int nsslTop = getNotificationsClippingTopBounds(top); + int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop(); + int bottomRadius = mSplitShadeEnabled ? radius : 0; + // TODO (b/265193930): remove dependency on NPVC + int topRadius = mSplitShadeEnabled + && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius; + mNotificationStackScrollLayoutController.setRoundedClippingBounds( + nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius); + } } /** @@ -1776,7 +1778,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum // Dragging down on the lockscreen statusbar should prohibit other interactions // immediately, otherwise we'll wait on the touchslop. This is to allow // dragging down to expanded quick settings directly on the lockscreen. - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mPanelView.getParent().requestDisallowInterceptTouchEvent(true); } } @@ -1821,7 +1823,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum && Math.abs(h) > Math.abs(x - mInitialTouchX) && shouldQuickSettingsIntercept( mInitialTouchX, mInitialTouchY, h)) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mPanelView.getParent().requestDisallowInterceptTouchEvent(true); } mShadeLog.onQsInterceptMoveQsTrackingEnabled(h); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java index 0a57b64b1ecf..813df1127fb8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java @@ -232,6 +232,13 @@ public interface ShadeController extends CoreStartable { /** Called when a launch animation ends. */ void onLaunchAnimationEnd(boolean launchIsFullScreen); + /** + * Performs haptic feedback from a view with a haptic feedback constant. + * + * @param constant One of android.view.HapticFeedbackConstants + */ + void performHapticFeedback(int constant); + /** Sets the listener for when the visibility of the shade changes. */ default void setVisibilityListener(ShadeVisibilityListener listener) {} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt index 093690ffb881..d703a2763e75 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt @@ -63,4 +63,5 @@ open class ShadeControllerEmptyImpl @Inject constructor() : ShadeController { override fun onStatusBarTouch(event: MotionEvent?) {} override fun onLaunchAnimationCancelled(isLaunchForActivity: Boolean) {} override fun onLaunchAnimationEnd(launchIsFullScreen: Boolean) {} + override fun performHapticFeedback(constant: Int) {} } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java index d99d607879cc..5f5e5cedff84 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java @@ -271,6 +271,11 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { } @Override + public void performHapticFeedback(int constant) { + getNpvc().performHapticFeedback(constant); + } + + @Override public void instantCollapseShade() { getNpvc().instantCollapse(); runPostCollapseActions(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 177c3db6b720..6bb1df7daed8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -33,6 +33,7 @@ import com.android.systemui.shade.ShadeController.ShadeVisibilityListener import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import dagger.Lazy @@ -62,6 +63,7 @@ constructor( private val deviceEntryInteractor: DeviceEntryInteractor, private val notificationStackScrollLayout: NotificationStackScrollLayout, @ShadeTouchLog private val touchLog: LogBuffer, + private val vibratorHelper: VibratorHelper, commandQueue: CommandQueue, statusBarKeyguardViewManager: StatusBarKeyguardViewManager, notificationShadeWindowController: NotificationShadeWindowController, @@ -246,7 +248,11 @@ constructor( } override fun onStatusBarTouch(event: MotionEvent) { - // The only call to this doesn't happen with migrateClocksToBlueprint() enabled + // The only call to this doesn't happen with MigrateClocksToBlueprint.isEnabled enabled throw UnsupportedOperationException() } + + override fun performHapticFeedback(constant: Int) { + vibratorHelper.performHapticFeedback(notificationStackScrollLayout, constant) + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt index 25e27ae586c8..7425807b716d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt @@ -27,6 +27,7 @@ import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractorEmpt import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractorEmptyImpl +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import dagger.Binds import dagger.Module diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 3e9a32b9cde4..2d3833c55199 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -36,6 +36,7 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractorImpl import com.android.systemui.shade.domain.interactor.ShadeInteractorLegacyImpl import com.android.systemui.shade.domain.interactor.ShadeInteractorSceneContainerImpl +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractorImpl import dagger.Binds import dagger.Module diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt index 5c276b189ba7..d02c2154279b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt @@ -18,6 +18,7 @@ package com.android.systemui.shade import android.view.ViewPropertyAnimator import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.statusbar.GestureRecorder import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.policy.HeadsUpManager diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt index d90bb0b98056..b2837208a516 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt @@ -44,6 +44,7 @@ interface ShadeViewController { fun disableHeader(state1: Int, state2: Int, animated: Boolean) /** If the latency tracker is enabled, begins tracking expand latency. */ + @Deprecated("No longer supported. Do not add new calls to this.") fun startExpandLatencyTracking() /** Sets the alpha value of the shade to a value between 0 and 255. */ @@ -57,26 +58,19 @@ interface ShadeViewController { fun setAlphaChangeAnimationEndAction(r: Runnable) /** Sets Qs ScrimEnabled and updates QS state. */ + @Deprecated("Does nothing when scene container is enabled.") fun setQsScrimEnabled(qsScrimEnabled: Boolean) /** Sets the top spacing for the ambient indicator. */ fun setAmbientIndicationTop(ambientIndicationTop: Int, ambientTextVisible: Boolean) /** Updates notification panel-specific flags on [SysUiState]. */ - fun updateSystemUiStateFlags() + @Deprecated("Does nothing when scene container is enabled.") fun updateSystemUiStateFlags() /** Ensures that the touchable region is updated. */ fun updateTouchableRegion() /** - * Reconfigures the shade to show the AOD UI (clock, smartspace, etc). This is called by the - * screen off animation controller in order to animate in AOD without "actually" fully switching - * to the KEYGUARD state, which is a heavy transition that causes jank as 10+ files react to the - * change. - */ - fun showAodUi() - - /** * Sends an external (e.g. Status Bar) touch event to the Shade touch handler. * * This is different from [startInputFocusTransfer] as it doesn't rely on setting the launcher @@ -105,16 +99,6 @@ interface ShadeViewController { @Deprecated("No longer supported. Do not add new calls to this.") fun finishInputFocusTransfer(velocity: Float) - /** - * Performs haptic feedback from a view with a haptic feedback constant. - * - * The implementation of this method should use the [android.view.View.performHapticFeedback] - * method with the provided constant. - * - * @param[constant] One of [android.view.HapticFeedbackConstants] - */ - fun performHapticFeedback(constant: Int) - /** Returns the ShadeHeadsUpTracker. */ val shadeHeadsUpTracker: ShadeHeadsUpTracker diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt index 69849e826535..bfb5ad3782b1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt @@ -20,6 +20,7 @@ import android.view.MotionEvent import android.view.ViewGroup import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.shade.domain.interactor.ShadeBackActionInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.phone.HeadsUpAppearanceController import java.util.function.Consumer @@ -84,8 +85,6 @@ class ShadeViewControllerEmptyImpl @Inject constructor() : override fun startInputFocusTransfer() {} override fun cancelInputFocusTransfer() {} override fun finishInputFocusTransfer(velocity: Float) {} - override fun performHapticFeedback(constant: Int) {} - override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl() override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl() @Deprecated("Use SceneInteractor.currentScene instead.") diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index bc60c838b703..cde45f2060e5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -66,7 +66,7 @@ interface BaseShadeInteractor { val isAnyExpanded: StateFlow<Boolean> /** The amount [0-1] that the shade has been opened. */ - val shadeExpansion: Flow<Float> + val shadeExpansion: StateFlow<Float> /** * The amount [0-1] QS has been opened. Normal shade with notifications (QQS) visible will diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index e9bb4c623013..5fbd2cfaec79 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -29,7 +29,7 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { private val inactiveFlowBoolean = MutableStateFlow(false) private val inactiveFlowFloat = MutableStateFlow(0f) override val isShadeEnabled: StateFlow<Boolean> = inactiveFlowBoolean - override val shadeExpansion: Flow<Float> = inactiveFlowFloat + override val shadeExpansion: StateFlow<Float> = inactiveFlowFloat override val qsExpansion: StateFlow<Float> = inactiveFlowFloat override val isQsExpanded: StateFlow<Boolean> = inactiveFlowBoolean override val isQsBypassingShade: Flow<Boolean> = inactiveFlowBoolean diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt index 421a76163346..ac881b5bfa97 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt @@ -50,7 +50,7 @@ constructor( * The amount [0-1] that the shade has been opened. Uses stateIn to avoid redundant calculations * in downstream flows. */ - override val shadeExpansion: Flow<Float> = + override val shadeExpansion: StateFlow<Float> = combine( repository.lockscreenShadeExpansion, keyguardRepository.statusBarState, diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt index 7785eda4bd6a..7f35f17954c4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt @@ -49,7 +49,9 @@ constructor( sharedNotificationContainerInteractor: SharedNotificationContainerInteractor, shadeRepository: ShadeRepository, ) : BaseShadeInteractor { - override val shadeExpansion: Flow<Float> = sceneBasedExpansion(sceneInteractor, Scenes.Shade) + override val shadeExpansion: StateFlow<Float> = + sceneBasedExpansion(sceneInteractor, Scenes.Shade) + .stateIn(scope, SharingStarted.Eagerly, 0f) private val sceneBasedQsExpansion = sceneBasedExpansion(sceneInteractor, Scenes.QuickSettings) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractor.kt index 859fce53a371..2611092553ed 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLockscreenInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractor.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.shade +package com.android.systemui.shade.domain.interactor /** Allows the lockscreen to control the shade. */ interface ShadeLockscreenInteractor { @@ -73,4 +73,12 @@ interface ShadeLockscreenInteractor { * @param alpha value between 0 and 1. -1 if the value is to be reset. */ @Deprecated("TODO(b/325072511) delete this") fun setKeyguardStatusBarAlpha(alpha: Float) + + /** + * Reconfigures the shade to show the AOD UI (clock, smartspace, etc). This is called by the + * screen off animation controller in order to animate in AOD without "actually" fully switching + * to the KEYGUARD state, which is a heavy transition that causes jank as 10+ files react to the + * change. + */ + fun showAodUi() } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt index d9c441fa0517..318da557ed2a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeLockscreenInteractorImpl.kt @@ -20,7 +20,6 @@ import com.android.keyguard.LockIconViewController import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.model.Scenes -import com.android.systemui.shade.ShadeLockscreenInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -84,6 +83,11 @@ constructor( // TODO(b/325072511) delete this } + override fun showAodUi() { + sceneInteractor.changeScene(Scenes.Lockscreen, "showAodUi") + // TODO(b/330311871) implement transition to AOD + } + private fun changeToShadeScene() { sceneInteractor.changeScene( Scenes.Shade, diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt index ea549f2b7e53..24b7533d6c26 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -66,11 +66,13 @@ constructor( deviceEntryInteractor.isUnlocked, deviceEntryInteractor.canSwipeToEnter, shadeInteractor.shadeMode, - ) { isUnlocked, canSwipeToDismiss, shadeMode -> + qsSceneAdapter.isCustomizing + ) { isUnlocked, canSwipeToDismiss, shadeMode, isCustomizing -> destinationScenes( isUnlocked = isUnlocked, canSwipeToDismiss = canSwipeToDismiss, shadeMode = shadeMode, + isCustomizing = isCustomizing ) } .stateIn( @@ -81,6 +83,7 @@ constructor( isUnlocked = deviceEntryInteractor.isUnlocked.value, canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value, shadeMode = shadeInteractor.shadeMode.value, + isCustomizing = qsSceneAdapter.isCustomizing.value, ), ) @@ -120,6 +123,7 @@ constructor( isUnlocked: Boolean, canSwipeToDismiss: Boolean?, shadeMode: ShadeMode, + isCustomizing: Boolean, ): Map<UserAction, UserActionResult> { val up = when { @@ -131,7 +135,9 @@ constructor( val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single } return buildMap { - this[Swipe(SwipeDirection.Up)] = UserActionResult(up) + if (!isCustomizing) { + this[Swipe(SwipeDirection.Up)] = UserActionResult(up) + } // TODO(b/330200163) Add an else to be able to collapse the shade while customizing down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 44068139f66b..e7b159a2d057 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -529,9 +529,9 @@ public class CommandQueue extends IStatusBar.Stub implements default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {} /** - * @see IStatusBar#enterDesktop(int) + * @see IStatusBar#moveFocusedTaskToDesktop(int) */ - default void enterDesktop(int displayId) {} + default void moveFocusedTaskToDesktop(int displayId) {} } @VisibleForTesting @@ -1444,7 +1444,7 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override - public void enterDesktop(int displayId) { + public void moveFocusedTaskToDesktop(int displayId) { SomeArgs args = SomeArgs.obtain(); args.arg1 = displayId; mHandler.obtainMessage(MSG_ENTER_DESKTOP, args).sendToTarget(); @@ -1960,7 +1960,7 @@ public class CommandQueue extends IStatusBar.Stub implements args = (SomeArgs) msg.obj; int displayId = args.argi1; for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).enterDesktop(displayId); + mCallbacks.get(i).moveFocusedTaskToDesktop(displayId); } break; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index a12b9709a063..d6858cad6d0b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -560,11 +560,6 @@ public final class KeyboardShortcutListSearch { Pair.create( KeyEvent.KEYCODE_TAB, KeyEvent.META_SHIFT_ON | KeyEvent.META_ALT_ON))), - /* Hide and (re)show taskbar: Meta + T */ - new ShortcutKeyGroupMultiMappingInfo( - context.getString(R.string.group_system_hide_reshow_taskbar), - Arrays.asList( - Pair.create(KeyEvent.KEYCODE_T, KeyEvent.META_META_ON))), /* Access notification shade: Meta + N */ new ShortcutKeyGroupMultiMappingInfo( context.getString(R.string.group_system_access_notification_shade), @@ -636,34 +631,41 @@ public final class KeyboardShortcutListSearch { // Enter Split screen with current app to RHS: Meta + Ctrl + Right arrow // Enter Split screen with current app to LHS: Meta + Ctrl + Left arrow // Switch from Split screen to full screen: Meta + Ctrl + Up arrow - String[] shortcutLabels = { - context.getString(R.string.system_multitasking_rhs), - context.getString(R.string.system_multitasking_lhs), - context.getString(R.string.system_multitasking_full_screen), - }; - int[] keyCodes = { - KeyEvent.KEYCODE_DPAD_RIGHT, - KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_DPAD_UP, - }; - - for (int i = 0; i < shortcutLabels.length; i++) { - List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup( - new KeyboardShortcutInfo( - shortcutLabels[i], - keyCodes[i], - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON), - null)); - ShortcutMultiMappingInfo shortcutMultiMappingInfo = - new ShortcutMultiMappingInfo( - shortcutLabels[i], - null, - shortcutKeyGroups); - systemMultitaskingGroup.addItem(shortcutMultiMappingInfo); - } + // Change split screen focus to RHS: Meta + Alt + Right arrow + // Change split screen focus to LHS: Meta + Alt + Left arrow + systemMultitaskingGroup.addItem( + getMultitaskingShortcut(context.getString(R.string.system_multitasking_rhs), + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut(context.getString(R.string.system_multitasking_lhs), + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut(context.getString(R.string.system_multitasking_full_screen), + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut( + context.getString(R.string.system_multitasking_splitscreen_focus_rhs), + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut( + context.getString(R.string.system_multitasking_splitscreen_focus_lhs), + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON)); return systemMultitaskingGroup; } + private static ShortcutMultiMappingInfo getMultitaskingShortcut(String shortcutLabel, + int keycode, int modifiers) { + List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList( + new ShortcutKeyGroup(new KeyboardShortcutInfo(shortcutLabel, keycode, modifiers), + null)); + return new ShortcutMultiMappingInfo(shortcutLabel, null, shortcutKeyGroups); + } + private static KeyboardShortcutMultiMappingGroup getMultiMappingInputShortcuts( Context context) { List<ShortcutMultiMappingInfo> shortcutMultiMappingInfoList = Arrays.asList( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index c9046217e68a..815236e0820c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -757,8 +757,8 @@ public class KeyguardIndicationController { mRotateTextViewController.updateIndication( INDICATION_TYPE_ADAPTIVE_AUTH, new KeyguardIndication.Builder() - .setMessage(mContext - .getString(R.string.kg_prompt_after_adaptive_auth_lock)) + .setMessage(mContext.getString( + R.string.keyguard_indication_after_adaptive_auth_lock)) .setTextColor(mInitialTextColorState) .build(), true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt index 9f098e79f759..72f2aa5aa10b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionController.kt @@ -6,7 +6,7 @@ import android.util.MathUtils import com.android.systemui.dump.DumpManager import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.res.R -import com.android.systemui.shade.ShadeLockscreenInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.SplitShadeStateController import dagger.assisted.Assisted diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 4b161260e788..fc1dc62ed094 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -15,13 +15,13 @@ import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.ExpandHelper import com.android.systemui.Flags.nsslFalsingFix -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.Gefingerpoken import com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager @@ -33,7 +33,7 @@ import com.android.systemui.plugins.qs.QS import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.res.R -import com.android.systemui.shade.ShadeLockscreenInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -69,7 +69,7 @@ constructor( private val mediaHierarchyManager: MediaHierarchyManager, private val scrimTransitionController: LockscreenShadeScrimTransitionController, private val keyguardTransitionControllerFactory: - LockscreenShadeKeyguardTransitionController.Factory, + LockscreenShadeKeyguardTransitionController.Factory, private val depthController: NotificationShadeDepthController, private val context: Context, private val splitShadeOverScrollerFactory: SplitShadeLockScreenOverScroller.Factory, @@ -292,8 +292,7 @@ constructor( /** @return true if the interaction is accepted, false if it should be cancelled */ internal fun canDragDown(): Boolean { return (statusBarStateController.state == StatusBarState.KEYGUARD || - nsslController.isInLockedDownShade()) && - (isQsFullyCollapsed || useSplitShade) + nsslController.isInLockedDownShade()) && (isQsFullyCollapsed || useSplitShade) } /** Called by the touch helper when when a gesture has completed all the way and released. */ @@ -885,7 +884,7 @@ class DragDownHelper( isDraggingDown = false isTrackpadReverseScroll = false shadeRepository.setLegacyLockscreenShadeTracking(false) - if (nsslFalsingFix() || migrateClocksToBlueprint()) { + if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled) { return true } } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 5171a5c9144c..9a82ecf01449 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -863,7 +863,7 @@ public class NotificationShelf extends ActivatableNotificationView { boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf(); iconState.hidden = isAppearing || (view instanceof ExpandableNotificationRow - && ((ExpandableNotificationRow) view).isLowPriority() + && ((ExpandableNotificationRow) view).isMinimized() && mShelfIcons.areIconsOverflowing()) || (transitionAmount == 0.0f && !iconState.isAnimating(icon)) || row.isAboveShelf() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index e111525285e1..8cdf60b20786 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -42,7 +42,6 @@ import android.app.Person; import android.app.RemoteInput; import android.app.RemoteInputHistoryItem; import android.content.Context; -import android.content.pm.ShortcutInfo; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; @@ -133,7 +132,6 @@ public final class NotificationEntry extends ListEntry { public Uri remoteInputUri; public ContentInfo remoteInputAttachment; private Notification.BubbleMetadata mBubbleMetadata; - private ShortcutInfo mShortcutInfo; /** * If {@link RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is @@ -168,10 +166,8 @@ public final class NotificationEntry extends ListEntry { private ListenerSet<OnSensitivityChangedListener> mOnSensitivityChangedListeners = new ListenerSet<>(); - private boolean mAutoHeadsUp; private boolean mPulseSupressed; private int mBucket = BUCKET_ALERTING; - @Nullable private Long mPendingAnimationDuration; private boolean mIsMarkedForUserTriggeredMovement; private boolean mIsHeadsUpEntry; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt index 0c69a65b96af..8531eaa46804 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt @@ -22,6 +22,7 @@ import android.os.UserHandle import android.provider.Settings import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable +import com.android.systemui.Flags.notificationMinimalismPrototype import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dump.DumpManager @@ -59,6 +60,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -260,8 +262,11 @@ constructor( } } - private suspend fun trackUnseenFilterSettingChanges() { - secureSettings + private fun unseenFeatureEnabled(): Flow<Boolean> { + if (notificationMinimalismPrototype()) { + return flowOf(true) + } + return secureSettings // emit whenever the setting has changed .observerFlow( UserHandle.USER_ALL, @@ -283,17 +288,20 @@ constructor( // only track the most recent emission, if events are happening faster than they can be // consumed .conflate() - .collectLatest { setting -> - // update local field and invalidate if necessary - if (setting != unseenFilterEnabled) { - unseenFilterEnabled = setting - unseenNotifFilter.invalidateList("unseen setting changed") - } - // if the setting is enabled, then start tracking and filtering unseen notifications - if (setting) { - trackSeenNotifications() - } + } + + private suspend fun trackUnseenFilterSettingChanges() { + unseenFeatureEnabled().collectLatest { setting -> + // update local field and invalidate if necessary + if (setting != unseenFilterEnabled) { + unseenFilterEnabled = setting + unseenNotifFilter.invalidateList("unseen setting changed") } + // if the setting is enabled, then start tracking and filtering unseen notifications + if (setting) { + trackSeenNotifications() + } + } } private val collectionListener = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java index dcfccd8398b2..0bbde21ba6a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator; -import static com.android.systemui.media.controls.domain.pipeline.MediaDataManagerKt.isMediaNotification; +import static com.android.systemui.media.controls.domain.pipeline.MediaDataManager.isMediaNotification; import android.os.RemoteException; import android.service.notification.StatusBarNotification; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index dfb0f9bb2a87..7a7b18450b48 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -363,7 +363,7 @@ public class PreparationCoordinator implements Coordinator { NotifInflater.Params getInflaterParams(NotifUiAdjustment adjustment, String reason) { return new NotifInflater.Params( - /* isLowPriority = */ adjustment.isMinimized(), + /* isMinimized = */ adjustment.isMinimized(), /* reason = */ reason, /* showSnooze = */ adjustment.isSnoozeEnabled(), /* isChildInGroup = */ adjustment.isChildInGroup(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt index 7b8a062ec446..ff72888a5c26 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt @@ -56,7 +56,7 @@ interface NotifInflater { /** A class holding parameters used when inflating the notification row */ class Params( - val isLowPriority: Boolean, + val isMinimized: Boolean, val reason: String, val showSnooze: Boolean, val isChildInGroup: Boolean = false, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index 4bbe0357b335..4a895c0571d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -243,7 +243,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { @Nullable NotificationRowContentBinder.InflationCallback inflationCallback) { final boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(entry.getSbn(), entry.getImportance()); - final boolean isLowPriority = inflaterParams.isLowPriority(); + final boolean isMinimized = inflaterParams.isMinimized(); // Set show snooze action row.setShowSnooze(inflaterParams.getShowSnooze()); @@ -252,7 +252,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.requireContentViews(FLAG_CONTENT_VIEW_CONTRACTED); params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED); params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight); - params.setUseLowPriority(isLowPriority); + params.setUseMinimized(isMinimized); if (screenshareNotificationHiding() ? inflaterParams.getNeedsRedaction() @@ -275,7 +275,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { if (AsyncGroupHeaderViewInflation.isEnabled()) { if (inflaterParams.isGroupSummary()) { params.requireContentViews(FLAG_GROUP_SUMMARY_HEADER); - if (isLowPriority) { + if (isMinimized) { params.requireContentViews(FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER); } } else { @@ -288,7 +288,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { mRowContentBindStage.requestRebind(entry, en -> { mLogger.logRebindComplete(entry); row.setUsesIncreasedCollapsedHeight(useIncreasedCollapsedHeight); - row.setIsLowPriority(isLowPriority); + row.setIsMinimized(isMinimized); if (inflationCallback != null) { inflationCallback.onAsyncInflationFinished(en); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt index e5e5292d9a94..2b0d2aa6ea2a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt @@ -15,8 +15,8 @@ */ package com.android.systemui.statusbar.notification.data -import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository -import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepositoryImpl +import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository +import com.android.systemui.statusbar.phone.HeadsUpManagerPhone import dagger.Binds import dagger.Module @@ -27,8 +27,5 @@ import dagger.Module ] ) interface NotificationDataLayerModule { - @Binds - fun bindHeadsUpNotificationRepository( - impl: HeadsUpNotificationRepositoryImpl - ): HeadsUpNotificationRepository + @Binds fun bindHeadsUpNotificationRepository(impl: HeadsUpManagerPhone): HeadsUpRepository } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt deleted file mode 100644 index d60ee9896758..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpNotificationRepository.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.data.repository - -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow -import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.policy.HeadsUpManager -import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener -import javax.inject.Inject -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow - -class HeadsUpNotificationRepositoryImpl -@Inject -constructor( - headsUpManager: HeadsUpManager, -) : HeadsUpNotificationRepository { - override val hasPinnedHeadsUp: Flow<Boolean> = conflatedCallbackFlow { - val listener = - object : OnHeadsUpChangedListener { - override fun onHeadsUpPinnedModeChanged(inPinnedMode: Boolean) { - trySend(headsUpManager.hasPinnedHeadsUp()) - } - - override fun onHeadsUpPinned(entry: NotificationEntry?) { - trySend(headsUpManager.hasPinnedHeadsUp()) - } - - override fun onHeadsUpUnPinned(entry: NotificationEntry?) { - trySend(headsUpManager.hasPinnedHeadsUp()) - } - - override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) { - trySend(headsUpManager.hasPinnedHeadsUp()) - } - } - trySend(headsUpManager.hasPinnedHeadsUp()) - headsUpManager.addListener(listener) - awaitClose { headsUpManager.removeListener(listener) } - } -} - -interface HeadsUpNotificationRepository { - val hasPinnedHeadsUp: Flow<Boolean> -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt new file mode 100644 index 000000000000..ed8c05688a66 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.data.repository + +import kotlinx.coroutines.flow.Flow + +/** + * A repository of currently displayed heads up notifications. + * + * This repository serves as a boundary between the + * [com.android.systemui.statusbar.policy.HeadsUpManager] and the modern notifications presentation + * codebase. + */ +interface HeadsUpRepository { + + /** + * True if we are exiting the headsUp pinned mode, and some notifications might still be + * animating out. This is used to keep the touchable regions in a reasonable state. + */ + val headsUpAnimatingAway: Flow<Boolean> + + /** The heads up row that should be displayed on top. */ + val topHeadsUpRow: Flow<HeadsUpRowRepository?> + + /** Set of currently active top-level heads up rows to be displayed. */ + val activeHeadsUpRows: Flow<Set<HeadsUpRowRepository>> +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt new file mode 100644 index 000000000000..7b40812d55c3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRowRepository.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.data.repository + +import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey +import kotlinx.coroutines.flow.StateFlow + +/** Representation of a top-level heads up row. */ +interface HeadsUpRowRepository : HeadsUpRowKey { + /** + * The key for this notification. Guaranteed to be immutable and unique. + * + * @see com.android.systemui.statusbar.notification.collection.NotificationEntry.getKey + */ + val key: String + + /** A key to identify this row in the view hierarchy. */ + val elementKey: Any + + /** Whether this notification is "pinned", meaning that it should stay on top of the screen. */ + val isPinned: StateFlow<Boolean> +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index 5c8f354de485..d1dd7b55c11f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -14,14 +14,59 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.statusbar.notification.domain.interactor -import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository +import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository +import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository +import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +class HeadsUpNotificationInteractor @Inject constructor(repository: HeadsUpRepository) { + + val topHeadsUpRow: Flow<HeadsUpRowKey?> = repository.topHeadsUpRow + + /** Set of currently pinned top-level heads up rows to be displayed. */ + val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> = + repository.activeHeadsUpRows.flatMapLatest { repositories -> + if (repositories.isNotEmpty()) { + val toCombine: List<Flow<Pair<HeadsUpRowRepository, Boolean>>> = + repositories.map { repo -> repo.isPinned.map { isPinned -> repo to isPinned } } + combine(toCombine) { pairs -> + pairs.filter { (_, isPinned) -> isPinned }.map { (repo, _) -> repo }.toSet() + } + } else { + // if the set is empty, there are no flows to combine + flowOf(emptySet()) + } + } + + /** Are there any pinned heads up rows to display? */ + val hasPinnedRows: Flow<Boolean> = + repository.activeHeadsUpRows.flatMapLatest { rows -> + if (rows.isNotEmpty()) { + combine(rows.map { it.isPinned }) { pins -> pins.any { it } } + } else { + // if the set is empty, there are no flows to combine + flowOf(false) + } + } -class HeadsUpNotificationInteractor @Inject constructor(repository: HeadsUpNotificationRepository) { val isHeadsUpOrAnimatingAway: Flow<Boolean> = - // TODO(b/296118689): Needs to include the animating away state. - repository.hasPinnedHeadsUp + combine(hasPinnedRows, repository.headsUpAnimatingAway) { hasPinnedRows, animatingAway -> + hasPinnedRows || animatingAway + } + + fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowInteractor = + HeadsUpRowInteractor(key as HeadsUpRowRepository) + fun elementKeyFor(key: HeadsUpRowKey) = (key as HeadsUpRowRepository).elementKey } + +class HeadsUpRowInteractor(repository: HeadsUpRowRepository) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java index f792898520a2..adcbbfbde002 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java @@ -58,6 +58,7 @@ public class FooterView extends StackScrollerDecorView { private FooterViewButton mClearAllButton; private FooterViewButton mManageOrHistoryButton; + private boolean mShouldBeHidden; private boolean mShowHistory; // String cache, for performance reasons. // Reading them from a Resources object can be quite slow sometimes. @@ -110,6 +111,20 @@ public class FooterView extends StackScrollerDecorView { setSecondaryVisible(visible, animate, onAnimationEnded); } + /** See {@link this#setShouldBeHidden} below. */ + public boolean shouldBeHidden() { + return mShouldBeHidden; + } + + /** + * Whether this view's visibility should be set to INVISIBLE. Note that this is different from + * the {@link StackScrollerDecorView#setVisible} method, which in turn handles visibility + * transitions between VISIBLE and GONE. + */ + public void setShouldBeHidden(boolean hide) { + mShouldBeHidden = hide; + } + @Override public void dump(PrintWriter pwOriginal, String[] args) { IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index c05c3c3df2c9..eb6c7b520037 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -327,7 +327,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private OnClickListener mExpandClickListener = new OnClickListener() { @Override public void onClick(View v) { - if (!shouldShowPublic() && (!mIsLowPriority || isExpanded()) + if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && mGroupMembershipManager.isGroupSummary(mEntry)) { mGroupExpansionChanging = true; final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); @@ -382,7 +382,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mAboveShelf; private OnUserInteractionCallback mOnUserInteractionCallback; private NotificationGutsManager mNotificationGutsManager; - private boolean mIsLowPriority; + private boolean mIsMinimized; private boolean mUseIncreasedCollapsedHeight; private boolean mUseIncreasedHeadsUpHeight; private float mTranslationWhenRemoved; @@ -467,7 +467,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (viewWrapper != null) { setIconAnimationRunningForChild(running, viewWrapper.getIcon()); } - NotificationViewWrapper lowPriWrapper = mChildrenContainer.getLowPriorityViewWrapper(); + NotificationViewWrapper lowPriWrapper = mChildrenContainer + .getMinimizedGroupHeaderWrapper(); if (lowPriWrapper != null) { setIconAnimationRunningForChild(running, lowPriWrapper.getIcon()); } @@ -680,7 +681,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (color != Notification.COLOR_INVALID) { return color; } else { - return mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded(), + return mEntry.getContrastedColor(mContext, mIsMinimized && !isExpanded(), getBackgroundColorWithoutTint()); } } @@ -1545,7 +1546,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * Set the low-priority group notification header view * @param headerView header view to set */ - public void setLowPriorityGroupHeader(NotificationHeaderView headerView) { + public void setMinimizedGroupHeader(NotificationHeaderView headerView) { NotificationChildrenContainer childrenContainer = getChildrenContainerNonNull(); childrenContainer.setLowPriorityGroupHeader( /* headerViewLowPriority= */ headerView, @@ -1664,16 +1665,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - public void setIsLowPriority(boolean isLowPriority) { - mIsLowPriority = isLowPriority; - mPrivateLayout.setIsLowPriority(isLowPriority); + /** + * Set if the row is minimized. + */ + public void setIsMinimized(boolean isMinimized) { + mIsMinimized = isMinimized; + mPrivateLayout.setIsLowPriority(isMinimized); if (mChildrenContainer != null) { - mChildrenContainer.setIsLowPriority(isLowPriority); + mChildrenContainer.setIsMinimized(isMinimized); } } - public boolean isLowPriority() { - return mIsLowPriority; + public boolean isMinimized() { + return mIsMinimized; } public void setUsesIncreasedCollapsedHeight(boolean use) { @@ -1763,9 +1767,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView */ public ExpandableNotificationRow(Context context, AttributeSet attrs) { this(context, attrs, context); - if (com.android.systemui.Flags.notificationRowUserContext()) { - Log.wtf(TAG, "This constructor shouldn't be called"); - } + Log.wtf(TAG, "This constructor shouldn't be called"); } /** @@ -2050,7 +2052,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainerStub = findViewById(R.id.child_container_stub); mChildrenContainerStub.setOnInflateListener((stub, inflated) -> { mChildrenContainer = (NotificationChildrenContainer) inflated; - mChildrenContainer.setIsLowPriority(mIsLowPriority); + mChildrenContainer.setIsMinimized(mIsMinimized); mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this); mChildrenContainer.onNotificationUpdated(); mChildrenContainer.setLogger(mChildrenContainerLogger); @@ -3435,7 +3437,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void onExpansionChanged(boolean userAction, boolean wasExpanded) { boolean nowExpanded = isExpanded(); - if (mIsSummaryWithChildren && (!mIsLowPriority || wasExpanded)) { + if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) { nowExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); } if (nowExpanded != wasExpanded) { @@ -3492,7 +3494,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (!expandable) { if (mIsSummaryWithChildren) { expandable = true; - if (!mIsLowPriority || isExpanded()) { + if (!mIsMinimized || isExpanded()) { isExpanded = isGroupExpanded(); } } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java index 6bc2b2f9e250..ba1cfcc425e8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java @@ -30,14 +30,21 @@ import android.widget.ImageView; import android.widget.TextView; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.widget.ConversationAvatarData; +import com.android.internal.widget.ConversationAvatarData.GroupConversationAvatarData; +import com.android.internal.widget.ConversationAvatarData.OneToOneConversationAvatarData; +import com.android.internal.widget.ConversationHeaderData; import com.android.internal.widget.ConversationLayout; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.NotificationFadeAware; import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; +import com.android.systemui.statusbar.notification.row.shared.ConversationStyleSetAvatarAsync; import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar; import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile; import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon; +import java.util.Objects; + /** * A hybrid view which may contain information about one ore more conversations. */ @@ -103,7 +110,7 @@ public class HybridConversationNotificationView extends HybridNotificationView { @Override public void bind(@Nullable CharSequence title, @Nullable CharSequence text, - @Nullable View contentView) { + @Nullable View contentView) { AsyncHybridViewInflation.assertInLegacyMode(); if (!(contentView instanceof ConversationLayout)) { super.bind(title, text, contentView); @@ -111,7 +118,38 @@ public class HybridConversationNotificationView extends HybridNotificationView { } ConversationLayout conversationLayout = (ConversationLayout) contentView; - Icon conversationIcon = conversationLayout.getConversationIcon(); + loadConversationAvatar(conversationLayout); + CharSequence conversationTitle = conversationLayout.getConversationTitle(); + if (TextUtils.isEmpty(conversationTitle)) { + conversationTitle = title; + } + if (conversationLayout.isOneToOne()) { + mConversationSenderName.setVisibility(GONE); + } else { + mConversationSenderName.setVisibility(VISIBLE); + mConversationSenderName.setText(conversationLayout.getConversationSenderName()); + } + CharSequence conversationText = conversationLayout.getConversationText(); + if (TextUtils.isEmpty(conversationText)) { + conversationText = text; + } + super.bind(conversationTitle, conversationText, conversationLayout); + } + + private void loadConversationAvatar(ConversationLayout conversationLayout) { + AsyncHybridViewInflation.assertInLegacyMode(); + if (ConversationStyleSetAvatarAsync.isEnabled()) { + loadConversationAvatarWithDrawable(conversationLayout); + } else { + loadConversationAvatarWithIcon(conversationLayout); + } + } + + @Deprecated + private void loadConversationAvatarWithIcon(ConversationLayout conversationLayout) { + ConversationStyleSetAvatarAsync.assertInLegacyMode(); + AsyncHybridViewInflation.assertInLegacyMode(); + final Icon conversationIcon = conversationLayout.getConversationIcon(); if (conversationIcon != null) { mConversationFacePile.setVisibility(GONE); mConversationIconView.setVisibility(VISIBLE); @@ -124,11 +162,11 @@ public class HybridConversationNotificationView extends HybridNotificationView { mConversationFacePile = requireViewById(com.android.internal.R.id.conversation_face_pile); - ImageView facePileBottomBg = mConversationFacePile.requireViewById( + final ImageView facePileBottomBg = mConversationFacePile.requireViewById( com.android.internal.R.id.conversation_face_pile_bottom_background); - ImageView facePileBottom = mConversationFacePile.requireViewById( + final ImageView facePileBottom = mConversationFacePile.requireViewById( com.android.internal.R.id.conversation_face_pile_bottom); - ImageView facePileTop = mConversationFacePile.requireViewById( + final ImageView facePileTop = mConversationFacePile.requireViewById( com.android.internal.R.id.conversation_face_pile_top); conversationLayout.bindFacePile(facePileBottomBg, facePileBottom, facePileTop); setSize(mConversationFacePile, mFacePileSize); @@ -139,21 +177,47 @@ public class HybridConversationNotificationView extends HybridNotificationView { mTransformationHelper.addViewTransformingToSimilar(facePileBottom); mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg); } - CharSequence conversationTitle = conversationLayout.getConversationTitle(); - if (TextUtils.isEmpty(conversationTitle)) { - conversationTitle = title; - } - if (conversationLayout.isOneToOne()) { - mConversationSenderName.setVisibility(GONE); + } + + private void loadConversationAvatarWithDrawable(ConversationLayout conversationLayout) { + AsyncHybridViewInflation.assertInLegacyMode(); + final ConversationHeaderData conversationHeaderData = Objects.requireNonNull( + conversationLayout.getConversationHeaderData(), + /* message = */ "conversationHeaderData should not be null"); + final ConversationAvatarData conversationAvatar = + Objects.requireNonNull(conversationHeaderData.getConversationAvatar(), + /* message = */"conversationAvatar should not be null"); + + if (conversationAvatar instanceof OneToOneConversationAvatarData oneToOneAvatar) { + mConversationFacePile.setVisibility(GONE); + mConversationIconView.setVisibility(VISIBLE); + mConversationIconView.setImageDrawable(oneToOneAvatar.mDrawable); + setSize(mConversationIconView, mSingleAvatarSize); } else { - mConversationSenderName.setVisibility(VISIBLE); - mConversationSenderName.setText(conversationLayout.getConversationSenderName()); - } - CharSequence conversationText = conversationLayout.getConversationText(); - if (TextUtils.isEmpty(conversationText)) { - conversationText = text; + // If there isn't an icon, generate a "face pile" based on the sender avatars + mConversationIconView.setVisibility(GONE); + mConversationFacePile.setVisibility(VISIBLE); + + final GroupConversationAvatarData groupAvatar = + (GroupConversationAvatarData) conversationAvatar; + mConversationFacePile = + requireViewById(com.android.internal.R.id.conversation_face_pile); + final ImageView facePileBottomBg = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_bottom_background); + final ImageView facePileBottom = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_bottom); + final ImageView facePileTop = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_top); + conversationLayout.bindFacePileWithDrawable(facePileBottomBg, facePileBottom, + facePileTop, groupAvatar); + setSize(mConversationFacePile, mFacePileSize); + setSize(facePileBottom, mFacePileAvatarSize); + setSize(facePileTop, mFacePileAvatarSize); + setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth); + mTransformationHelper.addViewTransformingToSimilar(facePileTop); + mTransformationHelper.addViewTransformingToSimilar(facePileBottom); + mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg); } - super.bind(conversationTitle, conversationText, conversationLayout); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index f835cca1a60c..ded635cb08bc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -150,7 +150,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder entry, mConversationProcessor, row, - bindParams.isLowPriority, + bindParams.isMinimized, bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, callback, @@ -178,7 +178,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder SmartReplyStateInflater smartRepliesInflater) { InflationProgress result = createRemoteViews(reInflateFlags, builder, - bindParams.isLowPriority, + bindParams.isMinimized, bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, packageContext, @@ -215,6 +215,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder apply( mInflationExecutor, inflateSynchronously, + bindParams.isMinimized, result, reInflateFlags, mRemoteViewCache, @@ -365,7 +366,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder } private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, - Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight, + Notification.Builder builder, boolean isMinimized, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, Context packageContext, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, @@ -376,13 +377,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view"); - result.newContentView = createContentView(builder, isLowPriority, + result.newContentView = createContentView(builder, isMinimized, usesIncreasedHeight); } if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view"); - result.newExpandedView = createExpandedView(builder, isLowPriority); + result.newExpandedView = createExpandedView(builder, isMinimized); } if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { @@ -393,7 +394,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating public remote view"); - result.newPublicView = builder.makePublicContentView(isLowPriority); + result.newPublicView = builder.makePublicContentView(isMinimized); } if (AsyncGroupHeaderViewInflation.isEnabled()) { @@ -406,7 +407,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating low-priority group summary remote view"); - result.mNewLowPriorityGroupHeaderView = + result.mNewMinimizedGroupHeaderView = builder.makeLowPriorityContentView(true /* useRegularSubtext */); } } @@ -444,6 +445,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private static CancellationSignal apply( Executor inflationExecutor, boolean inflateSynchronously, + boolean isMinimized, InflationProgress result, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, @@ -475,7 +477,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } }; logger.logAsyncTaskProgress(entry, "applying contracted view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, flag, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result, + reInflateFlags, flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, privateLayout, privateLayout.getContractedChild(), privateLayout.getVisibleWrapper( @@ -502,7 +505,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } }; logger.logAsyncTaskProgress(entry, "applying expanded view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result, + reInflateFlags, flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, privateLayout, privateLayout.getExpandedChild(), privateLayout.getVisibleWrapper(VISIBLE_TYPE_EXPANDED), runningInflations, @@ -529,7 +533,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } }; logger.logAsyncTaskProgress(entry, "applying heads up view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, + result, reInflateFlags, flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, privateLayout, privateLayout.getHeadsUpChild(), privateLayout.getVisibleWrapper(VISIBLE_TYPE_HEADSUP), runningInflations, @@ -555,7 +560,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } }; logger.logAsyncTaskProgress(entry, "applying public view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, flag, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, + result, reInflateFlags, flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, publicLayout, publicLayout.getContractedChild(), publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED), @@ -583,11 +589,12 @@ public class NotificationContentInflater implements NotificationRowContentBinder } }; logger.logAsyncTaskProgress(entry, "applying group header view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, + result, reInflateFlags, /* inflationId = */ FLAG_GROUP_SUMMARY_HEADER, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, /* parentLayout = */ childrenContainer, - /* existingView = */ childrenContainer.getNotificationHeader(), + /* existingView = */ childrenContainer.getGroupHeader(), /* existingWrapper = */ childrenContainer.getNotificationHeaderWrapper(), runningInflations, applyCallback, logger); } @@ -595,7 +602,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { boolean isNewView = !canReapplyRemoteView( - /* newView = */ result.mNewLowPriorityGroupHeaderView, + /* newView = */ result.mNewMinimizedGroupHeaderView, /* oldView = */ remoteViewCache.getCachedView( entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)); ApplyCallback applyCallback = new ApplyCallback() { @@ -603,29 +610,30 @@ public class NotificationContentInflater implements NotificationRowContentBinder public void setResultView(View v) { logger.logAsyncTaskProgress(entry, "low-priority group header view applied"); - result.mInflatedLowPriorityGroupHeaderView = (NotificationHeaderView) v; + result.mInflatedMinimizedGroupHeaderView = (NotificationHeaderView) v; } @Override public RemoteViews getRemoteView() { - return result.mNewLowPriorityGroupHeaderView; + return result.mNewMinimizedGroupHeaderView; } }; logger.logAsyncTaskProgress(entry, "applying low priority group header view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, + result, reInflateFlags, /* inflationId = */ FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, /* parentLayout = */ childrenContainer, - /* existingView = */ childrenContainer.getNotificationHeaderLowPriority(), + /* existingView = */ childrenContainer.getMinimizedNotificationHeader(), /* existingWrapper = */ childrenContainer - .getLowPriorityViewWrapper(), + .getMinimizedGroupHeaderWrapper(), runningInflations, applyCallback, logger); } } // Let's try to finish, maybe nobody is even inflating anything - finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, callback, entry, - row, logger); + finishIfDone(result, isMinimized, reInflateFlags, remoteViewCache, runningInflations, + callback, entry, row, logger); CancellationSignal cancellationSignal = new CancellationSignal(); cancellationSignal.setOnCancelListener( () -> { @@ -641,6 +649,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder static void applyRemoteView( Executor inflationExecutor, boolean inflateSynchronously, + boolean isMinimized, final InflationProgress result, final @InflationFlag int reInflateFlags, @InflationFlag int inflationId, @@ -707,7 +716,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder existingWrapper.onReinflated(); } runningInflations.remove(inflationId); - finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, + finishIfDone(result, isMinimized, + reInflateFlags, remoteViewCache, runningInflations, callback, entry, row, logger); } @@ -838,6 +848,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder * @return true if the inflation was finished */ private static boolean finishIfDone(InflationProgress result, + boolean isMinimized, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, HashMap<Integer, CancellationSignal> runningInflations, @Nullable InflationCallback endListener, NotificationEntry entry, @@ -944,7 +955,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder if (AsyncGroupHeaderViewInflation.isEnabled()) { if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) { if (result.mInflatedGroupHeaderView != null) { - row.setIsLowPriority(false); + // We need to set if the row is minimized before setting the group header to + // make sure the setting of header view works correctly + row.setIsMinimized(isMinimized); row.setGroupHeader(/* headerView= */ result.mInflatedGroupHeaderView); remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER, result.mNewGroupHeaderView); @@ -957,13 +970,14 @@ public class NotificationContentInflater implements NotificationRowContentBinder } if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { - if (result.mInflatedLowPriorityGroupHeaderView != null) { - // New view case, set row to low priority - row.setIsLowPriority(true); - row.setLowPriorityGroupHeader( - /* headerView= */ result.mInflatedLowPriorityGroupHeaderView); + if (result.mInflatedMinimizedGroupHeaderView != null) { + // We need to set if the row is minimized before setting the group header to + // make sure the setting of header view works correctly + row.setIsMinimized(isMinimized); + row.setMinimizedGroupHeader( + /* headerView= */ result.mInflatedMinimizedGroupHeaderView); remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, - result.mNewLowPriorityGroupHeaderView); + result.mNewMinimizedGroupHeaderView); } else if (remoteViewCache.hasCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)) { // Re-inflation case. Only update if it's still cached (i.e. view has not @@ -984,12 +998,12 @@ public class NotificationContentInflater implements NotificationRowContentBinder } private static RemoteViews createExpandedView(Notification.Builder builder, - boolean isLowPriority) { + boolean isMinimized) { RemoteViews bigContentView = builder.createBigContentView(); if (bigContentView != null) { return bigContentView; } - if (isLowPriority) { + if (isMinimized) { RemoteViews contentView = builder.createContentView(); Notification.Builder.makeHeaderExpanded(contentView); return contentView; @@ -998,8 +1012,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } private static RemoteViews createContentView(Notification.Builder builder, - boolean isLowPriority, boolean useLarge) { - if (isLowPriority) { + boolean isMinimized, boolean useLarge) { + if (isMinimized) { return builder.makeLowPriorityContentView(false /* useRegularSubtext */); } return builder.createContentView(useLarge); @@ -1038,7 +1052,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final NotificationEntry mEntry; private final Context mContext; private final boolean mInflateSynchronously; - private final boolean mIsLowPriority; + private final boolean mIsMinimized; private final boolean mUsesIncreasedHeight; private final InflationCallback mCallback; private final boolean mUsesIncreasedHeadsUpHeight; @@ -1063,7 +1077,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder NotificationEntry entry, ConversationNotificationProcessor conversationProcessor, ExpandableNotificationRow row, - boolean isLowPriority, + boolean isMinimized, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, InflationCallback callback, @@ -1080,7 +1094,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mRemoteViewCache = cache; mSmartRepliesInflater = smartRepliesInflater; mContext = mRow.getContext(); - mIsLowPriority = isLowPriority; + mIsMinimized = isMinimized; mUsesIncreasedHeight = usesIncreasedHeight; mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; mRemoteViewClickHandler = remoteViewClickHandler; @@ -1150,7 +1164,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mEntry, recoveredBuilder, mLogger); } InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, - recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, + recoveredBuilder, mIsMinimized, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, packageContext, mRow, mNotifLayoutInflaterFactoryProvider, mLogger); @@ -1209,6 +1223,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mCancellationSignal = apply( mInflationExecutor, mInflateSynchronously, + mIsMinimized, result, mReInflateFlags, mRemoteViewCache, @@ -1295,7 +1310,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private RemoteViews newExpandedView; private RemoteViews newPublicView; private RemoteViews mNewGroupHeaderView; - private RemoteViews mNewLowPriorityGroupHeaderView; + private RemoteViews mNewMinimizedGroupHeaderView; @VisibleForTesting Context packageContext; @@ -1305,7 +1320,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private View inflatedExpandedView; private View inflatedPublicView; private NotificationHeaderView mInflatedGroupHeaderView; - private NotificationHeaderView mInflatedLowPriorityGroupHeaderView; + private NotificationHeaderView mInflatedMinimizedGroupHeaderView; private CharSequence headsUpStatusBarText; private CharSequence headsUpStatusBarTextPublic; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 8a3e7e8a0580..6f00d96b6312 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -1514,7 +1514,7 @@ public class NotificationContentView extends FrameLayout implements Notification } ImageView bubbleButton = layout.findViewById(com.android.internal.R.id.bubble_button); View actionContainer = layout.findViewById(com.android.internal.R.id.actions_container); - LinearLayout actionListMarginTarget = layout.findViewById( + ViewGroup actionListMarginTarget = layout.findViewById( com.android.internal.R.id.notification_action_list_margin_target); if (bubbleButton == null || actionContainer == null) { return; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java index 609b15e51673..3e932aa616b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInlineImageResolver.java @@ -31,7 +31,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ImageResolver; import com.android.internal.widget.LocalImageResolver; import com.android.internal.widget.MessagingMessage; -import com.android.systemui.Flags; import java.util.HashSet; import java.util.List; @@ -67,11 +66,7 @@ public class NotificationInlineImageResolver implements ImageResolver { * @param imageCache The implementation of internal cache. */ public NotificationInlineImageResolver(Context context, ImageCache imageCache) { - if (Flags.notificationRowUserContext()) { - mContext = context; - } else { - mContext = context.getApplicationContext(); - } + mContext = context; mImageCache = imageCache; if (mImageCache != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java index b0fd47587782..33339a7fe025 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java @@ -128,9 +128,9 @@ public interface NotificationRowContentBinder { class BindParams { /** - * Bind a low priority version of the content views. + * Bind a minimized version of the content views. */ - public boolean isLowPriority; + public boolean isMinimized; /** * Use increased height when binding contracted view. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java index 1494c275d061..bae89fbf626f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java @@ -26,7 +26,7 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin * Parameters for {@link RowContentBindStage}. */ public final class RowContentBindParams { - private boolean mUseLowPriority; + private boolean mUseMinimized; private boolean mUseIncreasedHeight; private boolean mUseIncreasedHeadsUpHeight; private boolean mViewsNeedReinflation; @@ -41,17 +41,20 @@ public final class RowContentBindParams { private @InflationFlag int mDirtyContentViews = mContentViews; /** - * Set whether content should use a low priority version of its content views. + * Set whether content should use a minimized version of its content views. */ - public void setUseLowPriority(boolean useLowPriority) { - if (mUseLowPriority != useLowPriority) { + public void setUseMinimized(boolean useMinimized) { + if (mUseMinimized != useMinimized) { mDirtyContentViews |= (FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED); } - mUseLowPriority = useLowPriority; + mUseMinimized = useMinimized; } - public boolean useLowPriority() { - return mUseLowPriority; + /** + * @return Whether the row uses the minimized style. + */ + public boolean useMinimized() { + return mUseMinimized; } /** @@ -149,9 +152,9 @@ public final class RowContentBindParams { @Override public String toString() { return String.format("RowContentBindParams[mContentViews=%x mDirtyContentViews=%x " - + "mUseLowPriority=%b mUseIncreasedHeight=%b " + + "mUseMinimized=%b mUseIncreasedHeight=%b " + "mUseIncreasedHeadsUpHeight=%b mViewsNeedReinflation=%b]", - mContentViews, mDirtyContentViews, mUseLowPriority, mUseIncreasedHeight, + mContentViews, mDirtyContentViews, mUseMinimized, mUseIncreasedHeight, mUseIncreasedHeadsUpHeight, mViewsNeedReinflation); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index f4f8374d0a9f..89fcda949b5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -73,7 +73,7 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { mBinder.unbindContent(entry, row, contentToUnbind); BindParams bindParams = new BindParams(); - bindParams.isLowPriority = params.useLowPriority(); + bindParams.isMinimized = params.useMinimized(); bindParams.usesIncreasedHeight = params.useIncreasedHeight(); bindParams.usesIncreasedHeadsUpHeight = params.useIncreasedHeadsUpHeight(); boolean forceInflate = params.needsReinflation(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java index ea3036e35c1b..5fbcebda7cd6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowInflaterTask.java @@ -66,9 +66,7 @@ public class RowInflaterTask implements InflationTask, AsyncLayoutInflater.OnInf mInflateOrigin = new Throwable("inflate requested here"); } mListener = listener; - AsyncLayoutInflater inflater = com.android.systemui.Flags.notificationRowUserContext() - ? new AsyncLayoutInflater(context, makeRowInflater(entry)) - : new AsyncLayoutInflater(context); + AsyncLayoutInflater inflater = new AsyncLayoutInflater(context, makeRowInflater(entry)); mEntry = entry; entry.setInflationTask(this); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ConversationStyleSetAvatarAsync.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ConversationStyleSetAvatarAsync.kt new file mode 100644 index 000000000000..3c056c9611a3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/ConversationStyleSetAvatarAsync.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row.shared + +import android.widget.flags.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the conversation style set avatar async flag state. */ +@Suppress("NOTHING_TO_INLINE") +object ConversationStyleSetAvatarAsync { + const val FLAG_NAME = Flags.FLAG_CONVERSATION_STYLE_SET_AVATAR_ASYNC + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is async hybrid (single-line) view inflation enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.conversationStyleSetAvatarAsync() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.kt new file mode 100644 index 000000000000..8dc395d2888e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/HeadsUpRowKey.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.shared + +/** + * A unique key representing a top-level heads up notification. + * + * @see com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor + */ +interface HeadsUpRowKey diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.kt new file mode 100644 index 000000000000..62641fe2f229 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the notifications heads up refactor flag state. */ +@Suppress("NOTHING_TO_INLINE") +object NotificationsHeadsUpRefactor { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationsHeadsUpRefactor() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 28f874da0c74..5dc37e0525da 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -110,14 +110,14 @@ public class NotificationChildrenContainer extends ViewGroup */ private boolean mEnableShadowOnChildNotifications; - private NotificationHeaderView mNotificationHeader; - private NotificationHeaderViewWrapper mNotificationHeaderWrapper; - private NotificationHeaderView mNotificationHeaderLowPriority; - private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority; + private NotificationHeaderView mGroupHeader; + private NotificationHeaderViewWrapper mGroupHeaderWrapper; + private NotificationHeaderView mMinimizedGroupHeader; + private NotificationHeaderViewWrapper mMinimizedGroupHeaderWrapper; private NotificationGroupingUtil mGroupingUtil; private ViewState mHeaderViewState; private int mClipBottomAmount; - private boolean mIsLowPriority; + private boolean mIsMinimized; private OnClickListener mHeaderClickListener; private ViewGroup mCurrentHeader; private boolean mIsConversation; @@ -217,14 +217,14 @@ public class NotificationChildrenContainer extends ViewGroup int right = left + mOverflowNumber.getMeasuredWidth(); mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight()); } - if (mNotificationHeader != null) { - mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(), - mNotificationHeader.getMeasuredHeight()); + if (mGroupHeader != null) { + mGroupHeader.layout(0, 0, mGroupHeader.getMeasuredWidth(), + mGroupHeader.getMeasuredHeight()); } - if (mNotificationHeaderLowPriority != null) { - mNotificationHeaderLowPriority.layout(0, 0, - mNotificationHeaderLowPriority.getMeasuredWidth(), - mNotificationHeaderLowPriority.getMeasuredHeight()); + if (mMinimizedGroupHeader != null) { + mMinimizedGroupHeader.layout(0, 0, + mMinimizedGroupHeader.getMeasuredWidth(), + mMinimizedGroupHeader.getMeasuredHeight()); } } @@ -271,11 +271,11 @@ public class NotificationChildrenContainer extends ViewGroup } int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY); - if (mNotificationHeader != null) { - mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec); + if (mGroupHeader != null) { + mGroupHeader.measure(widthMeasureSpec, headerHeightSpec); } - if (mNotificationHeaderLowPriority != null) { - mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec); + if (mMinimizedGroupHeader != null) { + mMinimizedGroupHeader.measure(widthMeasureSpec, headerHeightSpec); } setMeasuredDimension(width, height); @@ -308,11 +308,11 @@ public class NotificationChildrenContainer extends ViewGroup * appropriately. */ public void setNotificationGroupWhen(long whenMillis) { - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.setNotificationWhen(whenMillis); + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.setNotificationWhen(whenMillis); } - if (mNotificationHeaderWrapperLowPriority != null) { - mNotificationHeaderWrapperLowPriority.setNotificationWhen(whenMillis); + if (mMinimizedGroupHeaderWrapper != null) { + mMinimizedGroupHeaderWrapper.setNotificationWhen(whenMillis); } } @@ -410,28 +410,28 @@ public class NotificationChildrenContainer extends ViewGroup Trace.beginSection("recreateHeader#makeNotificationGroupHeader"); RemoteViews header = builder.makeNotificationGroupHeader(); Trace.endSection(); - if (mNotificationHeader == null) { + if (mGroupHeader == null) { Trace.beginSection("recreateHeader#apply"); - mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this); + mGroupHeader = (NotificationHeaderView) header.apply(getContext(), this); Trace.endSection(); - mNotificationHeader.findViewById(com.android.internal.R.id.expand_button) + mGroupHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); - mNotificationHeader.setOnClickListener(mHeaderClickListener); - mNotificationHeaderWrapper = + mGroupHeader.setOnClickListener(mHeaderClickListener); + mGroupHeaderWrapper = (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( getContext(), - mNotificationHeader, + mGroupHeader, mContainingNotification); - mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); - addView(mNotificationHeader, 0); + mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); + addView(mGroupHeader, 0); invalidate(); } else { Trace.beginSection("recreateHeader#reapply"); - header.reapply(getContext(), mNotificationHeader); + header.reapply(getContext(), mGroupHeader); Trace.endSection(); } - mNotificationHeaderWrapper.setExpanded(mChildrenExpanded); - mNotificationHeaderWrapper.onContentUpdated(mContainingNotification); + mGroupHeaderWrapper.setExpanded(mChildrenExpanded); + mGroupHeaderWrapper.onContentUpdated(mContainingNotification); recreateLowPriorityHeader(builder, isConversation); updateHeaderVisibility(false /* animate */); updateChildrenAppearance(); @@ -439,21 +439,21 @@ public class NotificationChildrenContainer extends ViewGroup } private void removeGroupHeader() { - if (mNotificationHeader == null) { + if (mGroupHeader == null) { return; } - removeView(mNotificationHeader); - mNotificationHeader = null; - mNotificationHeaderWrapper = null; + removeView(mGroupHeader); + mGroupHeader = null; + mGroupHeaderWrapper = null; } private void removeLowPriorityGroupHeader() { - if (mNotificationHeaderLowPriority == null) { + if (mMinimizedGroupHeader == null) { return; } - removeView(mNotificationHeaderLowPriority); - mNotificationHeaderLowPriority = null; - mNotificationHeaderWrapperLowPriority = null; + removeView(mMinimizedGroupHeader); + mMinimizedGroupHeader = null; + mMinimizedGroupHeaderWrapper = null; } /** @@ -474,21 +474,21 @@ public class NotificationChildrenContainer extends ViewGroup return; } - mNotificationHeader = headerView; - mNotificationHeader.findViewById(com.android.internal.R.id.expand_button) + mGroupHeader = headerView; + mGroupHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); - mNotificationHeader.setOnClickListener(mHeaderClickListener); - mNotificationHeaderWrapper = + mGroupHeader.setOnClickListener(mHeaderClickListener); + mGroupHeaderWrapper = (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( getContext(), - mNotificationHeader, + mGroupHeader, mContainingNotification); - mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); - addView(mNotificationHeader, 0); + mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); + addView(mGroupHeader, 0); invalidate(); - mNotificationHeaderWrapper.setExpanded(mChildrenExpanded); - mNotificationHeaderWrapper.onContentUpdated(mContainingNotification); + mGroupHeaderWrapper.setExpanded(mChildrenExpanded); + mGroupHeaderWrapper.onContentUpdated(mContainingNotification); updateHeaderVisibility(false /* animate */); updateChildrenAppearance(); @@ -511,20 +511,20 @@ public class NotificationChildrenContainer extends ViewGroup return; } - mNotificationHeaderLowPriority = headerViewLowPriority; - mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button) + mMinimizedGroupHeader = headerViewLowPriority; + mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); - mNotificationHeaderLowPriority.setOnClickListener(onClickListener); - mNotificationHeaderWrapperLowPriority = + mMinimizedGroupHeader.setOnClickListener(onClickListener); + mMinimizedGroupHeaderWrapper = (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( getContext(), - mNotificationHeaderLowPriority, + mMinimizedGroupHeader, mContainingNotification); - mNotificationHeaderWrapperLowPriority.setOnRoundnessChangedListener(this::invalidate); - addView(mNotificationHeaderLowPriority, 0); + mMinimizedGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); + addView(mMinimizedGroupHeader, 0); invalidate(); - mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification); + mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification); updateHeaderVisibility(false /* animate */); updateChildrenAppearance(); } @@ -539,35 +539,35 @@ public class NotificationChildrenContainer extends ViewGroup AsyncGroupHeaderViewInflation.assertInLegacyMode(); RemoteViews header; StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); - if (mIsLowPriority) { + if (mIsMinimized) { if (builder == null) { builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); } header = builder.makeLowPriorityContentView(true /* useRegularSubtext */); - if (mNotificationHeaderLowPriority == null) { - mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(), + if (mMinimizedGroupHeader == null) { + mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(), this); - mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button) + mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); - mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener); - mNotificationHeaderWrapperLowPriority = + mMinimizedGroupHeader.setOnClickListener(mHeaderClickListener); + mMinimizedGroupHeaderWrapper = (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( getContext(), - mNotificationHeaderLowPriority, + mMinimizedGroupHeader, mContainingNotification); - mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); - addView(mNotificationHeaderLowPriority, 0); + mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); + addView(mMinimizedGroupHeader, 0); invalidate(); } else { - header.reapply(getContext(), mNotificationHeaderLowPriority); + header.reapply(getContext(), mMinimizedGroupHeader); } - mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification); - resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader()); + mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification); + resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, calculateDesiredHeader()); } else { - removeView(mNotificationHeaderLowPriority); - mNotificationHeaderLowPriority = null; - mNotificationHeaderWrapperLowPriority = null; + removeView(mMinimizedGroupHeader); + mMinimizedGroupHeader = null; + mMinimizedGroupHeaderWrapper = null; } } @@ -588,8 +588,8 @@ public class NotificationChildrenContainer extends ViewGroup public void updateGroupOverflow() { if (mShowGroupCountInExpander) { - setExpandButtonNumber(mNotificationHeaderWrapper); - setExpandButtonNumber(mNotificationHeaderWrapperLowPriority); + setExpandButtonNumber(mGroupHeaderWrapper); + setExpandButtonNumber(mMinimizedGroupHeaderWrapper); return; } int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); @@ -641,9 +641,9 @@ public class NotificationChildrenContainer extends ViewGroup * @param alpha alpha value to apply to the content */ public void setContentAlpha(float alpha) { - if (mNotificationHeader != null) { - for (int i = 0; i < mNotificationHeader.getChildCount(); i++) { - mNotificationHeader.getChildAt(i).setAlpha(alpha); + if (mGroupHeader != null) { + for (int i = 0; i < mGroupHeader.getChildCount(); i++) { + mGroupHeader.getChildAt(i).setAlpha(alpha); } } for (ExpandableNotificationRow child : getAttachedChildren()) { @@ -683,7 +683,7 @@ public class NotificationChildrenContainer extends ViewGroup if (AsyncGroupHeaderViewInflation.isEnabled()) { return mHeaderHeight; } else { - return mNotificationHeaderLowPriority.getHeight(); + return mMinimizedGroupHeader.getHeight(); } } int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation; @@ -837,15 +837,15 @@ public class NotificationChildrenContainer extends ViewGroup mGroupOverFlowState.setAlpha(0.0f); } } - if (mNotificationHeader != null) { + if (mGroupHeader != null) { if (mHeaderViewState == null) { mHeaderViewState = new ViewState(); } - mHeaderViewState.initFrom(mNotificationHeader); + mHeaderViewState.initFrom(mGroupHeader); if (mContainingNotification.hasExpandingChild()) { // Not modifying translationZ during expand animation. - mHeaderViewState.setZTranslation(mNotificationHeader.getTranslationZ()); + mHeaderViewState.setZTranslation(mGroupHeader.getTranslationZ()); } else if (childrenExpandedAndNotAnimating) { mHeaderViewState.setZTranslation(parentState.getZTranslation()); } else { @@ -898,7 +898,7 @@ public class NotificationChildrenContainer extends ViewGroup && !showingAsLowPriority()) { return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; } - if (mIsLowPriority + if (mIsMinimized || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded()) || (mContainingNotification.isHeadsUpState() && mContainingNotification.canShowHeadsUp())) { @@ -946,7 +946,7 @@ public class NotificationChildrenContainer extends ViewGroup mNeverAppliedGroupState = false; } if (mHeaderViewState != null) { - mHeaderViewState.applyToView(mNotificationHeader); + mHeaderViewState.applyToView(mGroupHeader); } updateChildrenClipping(); } @@ -1006,8 +1006,8 @@ public class NotificationChildrenContainer extends ViewGroup } if (child instanceof NotificationHeaderView - && mNotificationHeaderWrapper.hasRoundedCorner()) { - float[] radii = mNotificationHeaderWrapper.getUpdatedRadii(); + && mGroupHeaderWrapper.hasRoundedCorner()) { + float[] radii = mGroupHeaderWrapper.getUpdatedRadii(); mHeaderPath.reset(); mHeaderPath.addRoundRect( child.getLeft(), @@ -1085,8 +1085,8 @@ public class NotificationChildrenContainer extends ViewGroup } mGroupOverFlowState.animateTo(mOverflowNumber, properties); } - if (mNotificationHeader != null) { - mHeaderViewState.applyToView(mNotificationHeader); + if (mGroupHeader != null) { + mHeaderViewState.applyToView(mGroupHeader); } updateChildrenClipping(); } @@ -1109,8 +1109,8 @@ public class NotificationChildrenContainer extends ViewGroup public void setChildrenExpanded(boolean childrenExpanded) { mChildrenExpanded = childrenExpanded; updateExpansionStates(); - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.setExpanded(childrenExpanded); + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.setExpanded(childrenExpanded); } final int count = mAttachedChildren.size(); for (int childIdx = 0; childIdx < count; childIdx++) { @@ -1130,11 +1130,11 @@ public class NotificationChildrenContainer extends ViewGroup } public NotificationViewWrapper getNotificationViewWrapper() { - return mNotificationHeaderWrapper; + return mGroupHeaderWrapper; } - public NotificationViewWrapper getLowPriorityViewWrapper() { - return mNotificationHeaderWrapperLowPriority; + public NotificationViewWrapper getMinimizedGroupHeaderWrapper() { + return mMinimizedGroupHeaderWrapper; } @VisibleForTesting @@ -1142,12 +1142,12 @@ public class NotificationChildrenContainer extends ViewGroup return mCurrentHeader; } - public NotificationHeaderView getNotificationHeader() { - return mNotificationHeader; + public NotificationHeaderView getGroupHeader() { + return mGroupHeader; } - public NotificationHeaderView getNotificationHeaderLowPriority() { - return mNotificationHeaderLowPriority; + public NotificationHeaderView getMinimizedNotificationHeader() { + return mMinimizedGroupHeader; } private void updateHeaderVisibility(boolean animate) { @@ -1171,7 +1171,7 @@ public class NotificationChildrenContainer extends ViewGroup NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader); visibleWrapper.transformFrom(hiddenWrapper); hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false)); - startChildAlphaAnimations(desiredHeader == mNotificationHeader); + startChildAlphaAnimations(desiredHeader == mGroupHeader); } else { animate = false; } @@ -1192,8 +1192,8 @@ public class NotificationChildrenContainer extends ViewGroup } } - resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader); - resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader); + resetHeaderVisibilityIfNeeded(mGroupHeader, desiredHeader); + resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, desiredHeader); mCurrentHeader = desiredHeader; } @@ -1215,9 +1215,9 @@ public class NotificationChildrenContainer extends ViewGroup private ViewGroup calculateDesiredHeader() { ViewGroup desiredHeader; if (showingAsLowPriority()) { - desiredHeader = mNotificationHeaderLowPriority; + desiredHeader = mMinimizedGroupHeader; } else { - desiredHeader = mNotificationHeader; + desiredHeader = mGroupHeader; } return desiredHeader; } @@ -1244,20 +1244,20 @@ public class NotificationChildrenContainer extends ViewGroup private void updateHeaderTransformation() { if (mUserLocked && showingAsLowPriority()) { float fraction = getGroupExpandFraction(); - mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority, + mGroupHeaderWrapper.transformFrom(mMinimizedGroupHeaderWrapper, fraction); - mNotificationHeader.setVisibility(VISIBLE); - mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper, + mGroupHeader.setVisibility(VISIBLE); + mMinimizedGroupHeaderWrapper.transformTo(mGroupHeaderWrapper, fraction); } } private NotificationViewWrapper getWrapperForView(View visibleHeader) { - if (visibleHeader == mNotificationHeader) { - return mNotificationHeaderWrapper; + if (visibleHeader == mGroupHeader) { + return mGroupHeaderWrapper; } - return mNotificationHeaderWrapperLowPriority; + return mMinimizedGroupHeaderWrapper; } /** @@ -1266,13 +1266,13 @@ public class NotificationChildrenContainer extends ViewGroup * @param expanded whether the group is expanded. */ public void updateHeaderForExpansion(boolean expanded) { - if (mNotificationHeader != null) { + if (mGroupHeader != null) { if (expanded) { ColorDrawable cd = new ColorDrawable(); cd.setColor(mContainingNotification.calculateBgColor()); - mNotificationHeader.setHeaderBackgroundDrawable(cd); + mGroupHeader.setHeaderBackgroundDrawable(cd); } else { - mNotificationHeader.setHeaderBackgroundDrawable(null); + mGroupHeader.setHeaderBackgroundDrawable(null); } } } @@ -1405,11 +1405,11 @@ public class NotificationChildrenContainer extends ViewGroup if (AsyncGroupHeaderViewInflation.isEnabled()) { return mHeaderHeight; } - if (mNotificationHeaderLowPriority == null) { + if (mMinimizedGroupHeader == null) { Log.e(TAG, "getMinHeight: low priority header is null", new Exception()); return 0; } - return mNotificationHeaderLowPriority.getHeight(); + return mMinimizedGroupHeader.getHeight(); } int minExpandHeight = mNotificationHeaderMargin + headerTranslation; int visibleChildren = 0; @@ -1443,20 +1443,20 @@ public class NotificationChildrenContainer extends ViewGroup } public boolean showingAsLowPriority() { - return mIsLowPriority && !mContainingNotification.isExpanded(); + return mIsMinimized && !mContainingNotification.isExpanded(); } public void reInflateViews(OnClickListener listener, StatusBarNotification notification) { if (!AsyncGroupHeaderViewInflation.isEnabled()) { // When Async header inflation is enabled, we do not reinflate headers because they are // inflated from the background thread - if (mNotificationHeader != null) { - removeView(mNotificationHeader); - mNotificationHeader = null; + if (mGroupHeader != null) { + removeView(mGroupHeader); + mGroupHeader = null; } - if (mNotificationHeaderLowPriority != null) { - removeView(mNotificationHeaderLowPriority); - mNotificationHeaderLowPriority = null; + if (mMinimizedGroupHeader != null) { + removeView(mMinimizedGroupHeader); + mMinimizedGroupHeader = null; } recreateNotificationHeader(listener, mIsConversation); } @@ -1489,8 +1489,8 @@ public class NotificationChildrenContainer extends ViewGroup } private void updateHeaderTouchability() { - if (mNotificationHeader != null) { - mNotificationHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked); + if (mGroupHeader != null) { + mGroupHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked); } } @@ -1534,8 +1534,11 @@ public class NotificationChildrenContainer extends ViewGroup updateChildrenClipping(); } - public void setIsLowPriority(boolean isLowPriority) { - mIsLowPriority = isLowPriority; + /** + * Set whether the children container is minimized. + */ + public void setIsMinimized(boolean isMinimized) { + mIsMinimized = isMinimized; if (mContainingNotification != null) { /* we're not yet set up yet otherwise */ if (!AsyncGroupHeaderViewInflation.isEnabled()) { recreateLowPriorityHeader(null /* existingBuilder */, mIsConversation); @@ -1552,13 +1555,13 @@ public class NotificationChildrenContainer extends ViewGroup */ public NotificationViewWrapper getVisibleWrapper() { if (showingAsLowPriority()) { - return mNotificationHeaderWrapperLowPriority; + return mMinimizedGroupHeaderWrapper; } - return mNotificationHeaderWrapper; + return mGroupHeaderWrapper; } public void onExpansionChanged() { - if (mIsLowPriority) { + if (mIsMinimized) { if (mUserLocked) { setUserLocked(mUserLocked); } @@ -1574,15 +1577,15 @@ public class NotificationChildrenContainer extends ViewGroup @Override public void applyRoundnessAndInvalidate() { boolean last = true; - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.requestTopRoundness( + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.requestTopRoundness( /* value = */ getTopRoundness(), /* sourceType = */ FROM_PARENT, /* animate = */ false ); } - if (mNotificationHeaderWrapperLowPriority != null) { - mNotificationHeaderWrapperLowPriority.requestTopRoundness( + if (mMinimizedGroupHeaderWrapper != null) { + mMinimizedGroupHeaderWrapper.requestTopRoundness( /* value = */ getTopRoundness(), /* sourceType = */ FROM_PARENT, /* animate = */ false @@ -1612,31 +1615,31 @@ public class NotificationChildrenContainer extends ViewGroup * Shows the given feedback icon, or hides the icon if null. */ public void setFeedbackIcon(@Nullable FeedbackIcon icon) { - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.setFeedbackIcon(icon); + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.setFeedbackIcon(icon); } - if (mNotificationHeaderWrapperLowPriority != null) { - mNotificationHeaderWrapperLowPriority.setFeedbackIcon(icon); + if (mMinimizedGroupHeaderWrapper != null) { + mMinimizedGroupHeaderWrapper.setFeedbackIcon(icon); } } public void setRecentlyAudiblyAlerted(boolean audiblyAlertedRecently) { - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); } - if (mNotificationHeaderWrapperLowPriority != null) { - mNotificationHeaderWrapperLowPriority.setRecentlyAudiblyAlerted(audiblyAlertedRecently); + if (mMinimizedGroupHeaderWrapper != null) { + mMinimizedGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); } } @Override public void setNotificationFaded(boolean faded) { mContainingNotificationIsFaded = faded; - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.setNotificationFaded(faded); + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.setNotificationFaded(faded); } - if (mNotificationHeaderWrapperLowPriority != null) { - mNotificationHeaderWrapperLowPriority.setNotificationFaded(faded); + if (mMinimizedGroupHeaderWrapper != null) { + mMinimizedGroupHeaderWrapper.setNotificationFaded(faded); } for (ExpandableNotificationRow child : mAttachedChildren) { child.setNotificationFaded(faded); @@ -1654,7 +1657,7 @@ public class NotificationChildrenContainer extends ViewGroup } public NotificationHeaderViewWrapper getNotificationHeaderWrapper() { - return mNotificationHeaderWrapper; + return mGroupHeaderWrapper; } public void setLogger(NotificationChildrenContainerLogger logger) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 947976299f8e..fb528386018b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -812,6 +812,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } else { mDebugTextUsedYPositions.clear(); } + + mDebugPaint.setColor(Color.DKGRAY); + canvas.drawPath(mRoundedClipPath, mDebugPaint); + int y = 0; drawDebugInfo(canvas, y, Color.RED, /* label= */ "y = " + y); @@ -843,14 +847,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable drawDebugInfo(canvas, y, Color.LTGRAY, /* label= */ "mAmbientState.getStackY() + mAmbientState.getStackHeight() = " + y); - y = (int) mAmbientState.getStackY() + mContentHeight; - drawDebugInfo(canvas, y, Color.MAGENTA, - /* label= */ "mAmbientState.getStackY() + mContentHeight = " + y); - y = (int) (mAmbientState.getStackY() + mIntrinsicContentHeight); drawDebugInfo(canvas, y, Color.YELLOW, /* label= */ "mAmbientState.getStackY() + mIntrinsicContentHeight = " + y); + y = mContentHeight; + drawDebugInfo(canvas, y, Color.MAGENTA, + /* label= */ "mContentHeight = " + y); + drawDebugInfo(canvas, mRoundedRectClippingBottom, Color.DKGRAY, /* label= */ "mRoundedRectClippingBottom) = " + y); } @@ -4940,6 +4944,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable println(pw, "intrinsicPadding", mIntrinsicPadding); println(pw, "topPadding", mTopPadding); println(pw, "bottomPadding", mBottomPadding); + dumpRoundedRectClipping(pw); + println(pw, "requestedClipBounds", mRequestedClipBounds); + println(pw, "isClipped", mIsClipped); println(pw, "translationX", getTranslationX()); println(pw, "translationY", getTranslationY()); println(pw, "translationZ", getTranslationZ()); @@ -4994,6 +5001,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable }); } + private void dumpRoundedRectClipping(IndentingPrintWriter pw) { + pw.append("roundedRectClipping{l=").print(mRoundedRectClippingLeft); + pw.append(" t=").print(mRoundedRectClippingTop); + pw.append(" r=").print(mRoundedRectClippingRight); + pw.append(" b=").print(mRoundedRectClippingBottom); + pw.append("} topRadius=").print(mBgCornerRadii[0]); + pw.append(" bottomRadius=").println(mBgCornerRadii[4]); + } + private void dumpFooterViewVisibility(IndentingPrintWriter pw) { FooterViewRefactor.assertInLegacyMode(); final boolean showDismissView = shouldShowDismissView(); @@ -5389,7 +5405,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /** * @param topHeadsUpRow the first headsUp row in z-order. */ - public void setTopHeadsUpRow(ExpandableNotificationRow topHeadsUpRow) { + public void setTopHeadsUpRow(@Nullable ExpandableNotificationRow topHeadsUpRow) { mTopHeadsUpRow = topHeadsUpRow; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 8ed1ca28eaf1..ec111a13a3bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -23,7 +23,6 @@ import static com.android.app.animation.Interpolators.STANDARD; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING; import static com.android.server.notification.Flags.screenshareNotificationHiding; import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.Flags.nsslFalsingFix; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener; @@ -71,6 +70,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.TransitionStep; @@ -126,6 +126,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.NotificationGuts; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationSnooze; +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; @@ -685,11 +686,13 @@ public class NotificationStackScrollLayoutController implements Dumpable { new OnHeadsUpChangedListener() { @Override public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) { + NotificationsHeadsUpRefactor.assertInLegacyMode(); mView.setInHeadsUpPinnedMode(inPinnedMode); } @Override public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { + NotificationsHeadsUpRefactor.assertInLegacyMode(); NotificationEntry topEntry = mHeadsUpManager.getTopEntry(); mView.setTopHeadsUpRow(topEntry != null ? topEntry.getRow() : null); generateHeadsUpAnimation(entry, isHeadsUp); @@ -870,7 +873,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { }); } - mHeadsUpManager.addListener(mOnHeadsUpChangedListener); + if (!NotificationsHeadsUpRefactor.isEnabled()) { + mHeadsUpManager.addListener(mOnHeadsUpChangedListener); + } mHeadsUpManager.setAnimationStateHandler(mView::setHeadsUpGoingAwayAnimationsAllowed); mDynamicPrivacyController.addListener(mDynamicPrivacyControllerListener); @@ -2090,7 +2095,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } boolean horizontalSwipeWantsIt = false; boolean scrollerWantsIt = false; - if (nsslFalsingFix() || migrateClocksToBlueprint()) { + if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled()) { // Reverse the order relative to the else statement. onScrollTouch will reset on an // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes. if (mLongPressedView == null && !mView.isBeingDragged() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index 2d9c63efee53..1b53cbed8354 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -20,6 +20,7 @@ import android.content.res.Resources import android.util.Log import android.view.View.GONE import androidx.annotation.VisibleForTesting +import com.android.systemui.Flags.notificationMinimalismPrototype import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main @@ -66,6 +67,11 @@ constructor( */ private var maxKeyguardNotifications by notNull<Int>() + /** + * Whether [maxKeyguardNotifications] will have 1 added to it when media is shown in the stack. + */ + private var maxNotificationsExcludesMedia = false + /** Minimum space between two notifications, see [calculateGapAndDividerHeight]. */ private var dividerHeight by notNull<Float>() @@ -168,7 +174,11 @@ constructor( log { "\n" } val stackHeightSequence = computeHeightPerNotificationLimit(stack, shelfHeight) + + // TODO: Avoid making this split shade assumption by simply checking the stack for media val isMediaShowing = mediaDataManager.hasActiveMediaOrRecommendation() + val isMediaShowingInStack = isMediaShowing && !splitShadeStateController + .shouldUseSplitNotificationShade(resources) log { "\tGet maxNotifWithoutSavingSpace ---" } val maxNotifWithoutSavingSpace = @@ -181,12 +191,11 @@ constructor( } // How many notifications we can show at heightWithoutLockscreenConstraints - var minCountAtHeightWithoutConstraints = - if (isMediaShowing && !splitShadeStateController - .shouldUseSplitNotificationShade(resources)) 2 else 1 + val minCountAtHeightWithoutConstraints = if (isMediaShowingInStack) 2 else 1 log { "\t---maxNotifWithoutSavingSpace=$maxNotifWithoutSavingSpace " + "isMediaShowing=$isMediaShowing" + + "isMediaShowingInStack=$isMediaShowingInStack" + "minCountAtHeightWithoutConstraints=$minCountAtHeightWithoutConstraints" } log { "\n" } @@ -223,7 +232,9 @@ constructor( } if (onLockscreen()) { - maxNotifications = min(maxKeyguardNotifications, maxNotifications) + val increaseMaxForMedia = maxNotificationsExcludesMedia && isMediaShowingInStack + val lockscreenMax = maxKeyguardNotifications.safeIncrementIf(increaseMaxForMedia) + maxNotifications = min(lockscreenMax, maxNotifications) } // Could be < 0 if the space available is less than the shelf size. Returns 0 in this case. @@ -276,7 +287,7 @@ constructor( height = notifsHeight + shelfHeightWithSpaceBefore log { "--- computeHeight(maxNotifs=$maxNotifs, shelfHeight=$shelfHeight)" + - " -> ${height}=($notifsHeight+$shelfHeightWithSpaceBefore)" + + " -> $height=($notifsHeight+$shelfHeightWithSpaceBefore)" + " | saveSpaceOnLockscreen=$saveSpaceOnLockscreen" } } @@ -367,8 +378,9 @@ constructor( } fun updateResources() { - maxKeyguardNotifications = - infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count)) + maxKeyguardNotifications = if (notificationMinimalismPrototype()) 1 + else infiniteIfNegative(resources.getInteger(R.integer.keyguard_max_notification_count)) + maxNotificationsExcludesMedia = notificationMinimalismPrototype() dividerHeight = max(1f, resources.getDimensionPixelSize(R.dimen.notification_divider_height).toFloat()) @@ -486,6 +498,13 @@ constructor( v } + private fun Int.safeIncrementIf(condition: Boolean): Int = + if (condition && this != Int.MAX_VALUE) { + this + 1 + } else { + this + } + /** Returns the last index where [predicate] returns true, or -1 if it was always false. */ private fun <T> Sequence<T>.lastIndexWhile(predicate: (T) -> Boolean): Int = takeWhile(predicate).count() - 1 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 9b1952ba63fd..5eaccd924344 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -53,9 +53,7 @@ public class StackScrollAlgorithm { public static final float START_FRACTION = 0.5f; private static final String TAG = "StackScrollAlgorithm"; - private static final Boolean DEBUG = false; private static final SourceType STACK_SCROLL_ALGO = SourceType.from("StackScrollAlgorithm"); - private final ViewGroup mHostView; private float mPaddingBetweenElements; private float mGapHeight; @@ -247,13 +245,11 @@ public class StackScrollAlgorithm { >= ambientState.getMaxHeadsUpTranslation(); } - public static void log(String s) { - if (DEBUG) { - android.util.Log.i(TAG, s); - } + public static void debugLog(String s) { + android.util.Log.i(TAG, s); } - public static void logView(View view, String s) { + public static void debugLogView(View view, String s) { String viewString = ""; if (view instanceof ExpandableNotificationRow row) { if (row.getEntry() == null) { @@ -274,7 +270,7 @@ public class StackScrollAlgorithm { } else { viewString = view.toString(); } - log(viewString + " " + s); + debugLog(viewString + " " + s); } private void resetChildViewStates() { @@ -598,15 +594,16 @@ public class StackScrollAlgorithm { ); if (view instanceof FooterView) { if (FooterViewRefactor.isEnabled()) { - final float footerEnd = algorithmState.mCurrentExpandedYPosition - + view.getIntrinsicHeight(); - final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight(); - // TODO(b/293167744): May be able to keep only noSpaceForFooter here if we add an - // emission when clearAllNotifications is called, and then use that in the footer - // visibility flow. - ((FooterView.FooterViewState) viewState).hideContent = - noSpaceForFooter || (ambientState.isClearAllInProgress() - && !hasNonClearableNotifs(algorithmState)); + if (((FooterView) view).shouldBeHidden()) { + viewState.hidden = true; + } else { + final float footerEnd = algorithmState.mCurrentExpandedYPosition + + view.getIntrinsicHeight(); + final boolean noSpaceForFooter = footerEnd > ambientState.getStackEndHeight(); + ((FooterView.FooterViewState) viewState).hideContent = + noSpaceForFooter || (ambientState.isClearAllInProgress() + && !hasNonClearableNotifs(algorithmState)); + } } else { final boolean shadeClosed = !ambientState.isShadeExpanded(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt index 9efe632f5dbb..79ba25e1e23e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.notification.stack.data.repository -import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @SysUISingleton class NotificationStackAppearanceRepository @Inject constructor() { /** The bounds of the notification stack in the current scene. */ - val stackBounds = MutableStateFlow(NotificationContainerBounds()) + val stackBounds = MutableStateFlow(StackBounds()) /** * The height in px of the contents of notification stack. Depending on the number of diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 08df47388556..f05d01717a44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -17,13 +17,19 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor -import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository +import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds +import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf /** An interactor which controls the appearance of the NSSL */ @SysUISingleton @@ -31,9 +37,30 @@ class NotificationStackAppearanceInteractor @Inject constructor( private val repository: NotificationStackAppearanceRepository, + shadeInteractor: ShadeInteractor, ) { /** The bounds of the notification stack in the current scene. */ - val stackBounds: StateFlow<NotificationContainerBounds> = repository.stackBounds.asStateFlow() + val stackBounds: StateFlow<StackBounds> = repository.stackBounds.asStateFlow() + + /** + * Whether the stack is expanding from GONE-with-HUN to SHADE + * + * TODO(b/296118689): implement this to match legacy QSController logic + */ + private val isExpandingFromHeadsUp: Flow<Boolean> = flowOf(false) + + /** The rounding of the notification stack. */ + val stackRounding: Flow<StackRounding> = + combine( + shadeInteractor.shadeMode, + isExpandingFromHeadsUp, + ) { shadeMode, isExpandingFromHeadsUp -> + StackRounding( + roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp), + roundBottom = shadeMode != ShadeMode.Single, + ) + } + .distinctUntilChanged() /** * The height in px of the contents of notification stack. Depending on the number of @@ -59,7 +86,7 @@ constructor( val syntheticScroll: Flow<Float> = repository.syntheticScroll.asStateFlow() /** Sets the position of the notification stack in the current scene. */ - fun setStackBounds(bounds: NotificationContainerBounds) { + fun setStackBounds(bounds: StackBounds) { check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" } repository.stackBounds.value = bounds } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt new file mode 100644 index 000000000000..1fc9a182a10c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.shared.model + +/** Models the bounds of the notification stack. */ +data class StackBounds( + /** The position of the left of the stack in its window coordinate system, in pixels. */ + val left: Float = 0f, + /** The position of the top of the stack in its window coordinate system, in pixels. */ + val top: Float = 0f, + /** The position of the right of the stack in its window coordinate system, in pixels. */ + val right: Float = 0f, + /** The position of the bottom of the stack in its window coordinate system, in pixels. */ + val bottom: Float = 0f, +) { + /** The current height of the notification container. */ + val height: Float = bottom - top +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt new file mode 100644 index 000000000000..0c92b5023d1d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt @@ -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 com.android.systemui.statusbar.notification.stack.shared.model + +/** Models the clipping rounded rectangle of the notification stack */ +data class StackClipping(val bounds: StackBounds, val rounding: StackRounding) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt new file mode 100644 index 000000000000..ddc5d7ea0d7f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.shared.model + +/** Models the corner rounds of the notification stack. */ +data class StackRounding( + /** Whether the top corners of the notification stack should be rounded. */ + val roundTop: Boolean = false, + /** Whether the bottom corners of the notification stack should be rounded. */ + val roundBottom: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 6b30393ebc42..18bb51197555 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -36,6 +36,7 @@ import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker @@ -44,6 +45,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel +import com.android.systemui.statusbar.notification.ui.viewbinder.HeadsUpNotificationViewBinder import com.android.systemui.statusbar.phone.NotificationIconAreaController import com.android.systemui.util.kotlin.awaitCancellationThenDispose import com.android.systemui.util.kotlin.getOrNull @@ -71,6 +73,7 @@ constructor( private val hiderTracker: DisplaySwitchNotificationsHiderTracker, private val configuration: ConfigurationState, private val falsingManager: FalsingManager, + private val hunBinder: HeadsUpNotificationViewBinder, private val iconAreaController: NotificationIconAreaController, private val loggerOptional: Optional<NotificationStatsLogger>, private val metricsLogger: MetricsLogger, @@ -92,6 +95,9 @@ constructor( view.repeatWhenAttached { lifecycleScope.launch { + if (NotificationsHeadsUpRefactor.isEnabled) { + launch { hunBinder.bindHeadsUpNotifications(view) } + } launch { bindShelf(shelf) } bindHideList(viewController, viewModel, hiderTracker) @@ -187,13 +193,14 @@ constructor( }, ) launch { - viewModel.shouldShowFooterView.collect { animatedVisibility -> + viewModel.shouldIncludeFooterView.collect { animatedVisibility -> footerView.setVisible( /* visible = */ animatedVisibility.value, /* animate = */ animatedVisibility.isAnimating, ) } } + launch { viewModel.shouldHideFooterView.collect { footerView.setShouldBeHidden(it) } } disposableHandle.awaitCancellationThenDispose() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt deleted file mode 100644 index f10e5f1ab022..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.statusbar.notification.stack.ui.viewbinder - -import android.content.Context -import android.util.TypedValue -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel -import kotlin.math.roundToInt -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.launch - -/** Binds the shared notification container to its view-model. */ -object NotificationStackAppearanceViewBinder { - const val SCRIM_CORNER_RADIUS = 32f - - @JvmStatic - fun bind( - context: Context, - view: SharedNotificationContainer, - viewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - @Main mainImmediateDispatcher: CoroutineDispatcher, - ): DisposableHandle { - return view.repeatWhenAttached(mainImmediateDispatcher) { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - viewModel.stackBounds.collect { bounds -> - val viewLeft = controller.view.left - val viewTop = controller.view.top - controller.setRoundedClippingBounds( - bounds.left.roundToInt() - viewLeft, - bounds.top.roundToInt() - viewTop, - bounds.right.roundToInt() - viewLeft, - bounds.bottom.roundToInt() - viewTop, - SCRIM_CORNER_RADIUS.dpToPx(context), - 0, - ) - } - } - - launch { - viewModel.contentTop.collect { - controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) - } - } - - launch { - var wasExpanding = false - viewModel.expandFraction.collect { expandFraction -> - val nowExpanding = expandFraction != 0f && expandFraction != 1f - if (nowExpanding && !wasExpanding) { - controller.onExpansionStarted() - } - ambientState.expansionFraction = expandFraction - controller.expandedHeight = expandFraction * controller.view.height - if (!nowExpanding && wasExpanding) { - controller.onExpansionStopped() - } - wasExpanding = nowExpanding - } - } - - launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } } - } - } - } - - private fun Float.dpToPx(context: Context): Int { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - this, - context.resources.displayMetrics - ) - .roundToInt() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt new file mode 100644 index 000000000000..1a34bb4f02c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt @@ -0,0 +1,102 @@ +/* + * 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.statusbar.notification.stack.ui.viewbinder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.stack.AmbientState +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import javax.inject.Inject +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +/** Binds the NSSL/Controller/AmbientState to their ViewModel. */ +@SysUISingleton +class NotificationStackViewBinder +@Inject +constructor( + @Main private val mainImmediateDispatcher: CoroutineDispatcher, + private val ambientState: AmbientState, + private val view: NotificationStackScrollLayout, + private val controller: NotificationStackScrollLayoutController, + private val viewModel: NotificationStackAppearanceViewModel, + private val configuration: ConfigurationState, +) { + + fun bindWhileAttached(): DisposableHandle { + return view.repeatWhenAttached(mainImmediateDispatcher) { + repeatOnLifecycle(Lifecycle.State.CREATED) { bind() } + } + } + + suspend fun bind() = coroutineScope { + launch { + combine(viewModel.stackClipping, clipRadius, ::Pair).collect { (clipping, clipRadius) -> + val (bounds, rounding) = clipping + val viewLeft = controller.view.left + val viewTop = controller.view.top + controller.setRoundedClippingBounds( + bounds.left.roundToInt() - viewLeft, + bounds.top.roundToInt() - viewTop, + bounds.right.roundToInt() - viewLeft, + bounds.bottom.roundToInt() - viewTop, + if (rounding.roundTop) clipRadius else 0, + if (rounding.roundBottom) clipRadius else 0, + ) + } + } + + launch { + viewModel.contentTop.collect { + controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) + } + } + + launch { + var wasExpanding = false + viewModel.expandFraction.collect { expandFraction -> + val nowExpanding = expandFraction != 0f && expandFraction != 1f + if (nowExpanding && !wasExpanding) { + controller.onExpansionStarted() + } + ambientState.expansionFraction = expandFraction + controller.expandedHeight = expandFraction * controller.view.height + if (!nowExpanding && wasExpanding) { + controller.onExpansionStopped() + } + wasExpanding = nowExpanding + } + } + + launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } } + } + + private val clipRadius: Flow<Int> + get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 7c76ddbec105..ecf737a8650f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -20,6 +20,7 @@ import android.view.View import android.view.WindowInsets import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor @@ -30,6 +31,8 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel +import com.android.systemui.util.kotlin.DisposableHandles +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.flow.MutableStateFlow @@ -38,18 +41,24 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch /** Binds the shared notification container to its view-model. */ -object SharedNotificationContainerBinder { +@SysUISingleton +class SharedNotificationContainerBinder +@Inject +constructor( + private val sceneContainerFlags: SceneContainerFlags, + private val controller: NotificationStackScrollLayoutController, + private val notificationStackSizeCalculator: NotificationStackSizeCalculator, + private val notificationStackViewBinder: NotificationStackViewBinder, + @Main private val mainImmediateDispatcher: CoroutineDispatcher, +) { - @JvmStatic fun bind( view: SharedNotificationContainer, viewModel: SharedNotificationContainerViewModel, - sceneContainerFlags: SceneContainerFlags, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - @Main mainImmediateDispatcher: CoroutineDispatcher, ): DisposableHandle { - val disposableHandle = + val disposables = DisposableHandles() + + disposables += view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { @@ -72,24 +81,6 @@ object SharedNotificationContainerBinder { } } - // Required to capture keyguard media changes and ensure the notification count is correct - val layoutChangeListener = - object : View.OnLayoutChangeListener { - override fun onLayoutChange( - view: View, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - viewModel.notificationStackChanged() - } - } - val burnInParams = MutableStateFlow(BurnInParameters()) val viewState = ViewStateAccessor( @@ -100,7 +91,7 @@ object SharedNotificationContainerBinder { * For animation sensitive coroutines, immediately run just like applicationScope does * instead of doing a post() to the main thread. This extra delay can cause visible jitter. */ - val disposableHandleMainImmediate = + disposables += view.repeatWhenAttached(mainImmediateDispatcher) { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { @@ -167,7 +158,12 @@ object SharedNotificationContainerBinder { } } - controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() }) + if (sceneContainerFlags.isEnabled()) { + disposables += notificationStackViewBinder.bindWhileAttached() + } + + controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() } + disposables += DisposableHandle { controller.setOnHeightChangedRunnable(null) } view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets -> val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() @@ -176,16 +172,16 @@ object SharedNotificationContainerBinder { } insets } - view.addOnLayoutChangeListener(layoutChangeListener) + disposables += DisposableHandle { view.setOnApplyWindowInsetsListener(null) } - return object : DisposableHandle { - override fun dispose() { - disposableHandle.dispose() - disposableHandleMainImmediate.dispose() - controller.setOnHeightChangedRunnable(null) - view.setOnApplyWindowInsetsListener(null) - view.removeOnLayoutChangeListener(layoutChangeListener) + // Required to capture keyguard media changes and ensure the notification count is correct + val layoutChangeListener = + View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + viewModel.notificationStackChanged() } - } + view.addOnLayoutChangeListener(layoutChangeListener) + disposables += DisposableHandle { view.removeOnLayoutChangeListener(layoutChangeListener) } + + return disposables } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.kt new file mode 100644 index 000000000000..ec5e5be44298 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/HeadsUpRowViewModel.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.ui.viewmodel + +import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpRowInteractor + +class HeadsUpRowViewModel(headsUpRowInteractor: HeadsUpRowInteractor) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt index 4744fcbbc7f7..5a7433d3579b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt @@ -17,17 +17,20 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor +import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel +import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackInteractor import com.android.systemui.statusbar.policy.domain.interactor.UserSetupInteractor import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor -import com.android.systemui.util.kotlin.combine import com.android.systemui.util.kotlin.sample import com.android.systemui.util.ui.AnimatableEvent import com.android.systemui.util.ui.AnimatedValue @@ -53,6 +56,8 @@ constructor( val logger: Optional<NotificationLoggerViewModel>, activeNotificationsInteractor: ActiveNotificationsInteractor, notificationStackInteractor: NotificationStackInteractor, + private val headsUpNotificationInteractor: HeadsUpNotificationInteractor, + keyguardInteractor: KeyguardInteractor, remoteInputInteractor: RemoteInputInteractor, seenNotificationsInteractor: SeenNotificationsInteractor, shadeInteractor: ShadeInteractor, @@ -105,7 +110,32 @@ constructor( } } - val shouldShowFooterView: Flow<AnimatedValue<Boolean>> by lazy { + /** + * Whether the footer should not be visible for the user, even if it's present in the list (as + * per [shouldIncludeFooterView] below). + * + * This essentially corresponds to having the view set to INVISIBLE. + */ + val shouldHideFooterView: Flow<Boolean> by lazy { + if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + // When the shade is closed, the footer is still present in the list, but not visible. + // This prevents the footer from being shown when a HUN is present, while still allowing + // the footer to be counted as part of the shade for measurements. + shadeInteractor.shadeExpansion.map { it == 0f }.distinctUntilChanged() + } + } + + /** + * Whether the footer should be part of the list or not, and whether the transition from one + * state to another should be animated. This essentially corresponds to transitioning the view + * visibility from VISIBLE to GONE and vice versa. + * + * Note that this value being true doesn't necessarily mean that the footer is visible. It could + * be hidden by another condition (see [shouldHideFooterView] above). + */ + val shouldIncludeFooterView: Flow<AnimatedValue<Boolean>> by lazy { if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { flowOf(AnimatedValue.NotAnimating(false)) } else { @@ -114,34 +144,30 @@ constructor( userSetupInteractor.isUserSetUp, notificationStackInteractor.isShowingOnLockscreen, shadeInteractor.isQsFullscreen, - remoteInputInteractor.isRemoteInputActive, - shadeInteractor.shadeExpansion.map { it == 0f }.distinctUntilChanged(), + remoteInputInteractor.isRemoteInputActive ) { hasNotifications, isUserSetUp, isShowingOnLockscreen, qsFullScreen, - isRemoteInputActive, - isShadeClosed -> + isRemoteInputActive -> when { - !hasNotifications -> VisibilityChange.HIDE_WITH_ANIMATION + !hasNotifications -> VisibilityChange.DISAPPEAR_WITH_ANIMATION // Hide the footer until the user setup is complete, to prevent access // to settings (b/193149550). - !isUserSetUp -> VisibilityChange.HIDE_WITH_ANIMATION + !isUserSetUp -> VisibilityChange.DISAPPEAR_WITH_ANIMATION // Do not show the footer if the lockscreen is visible (incl. AOD), // except if the shade is opened on top. See also b/219680200. // Do not animate, as that makes the footer appear briefly when // transitioning between the shade and keyguard. - isShowingOnLockscreen -> VisibilityChange.HIDE_WITHOUT_ANIMATION + isShowingOnLockscreen -> VisibilityChange.DISAPPEAR_WITHOUT_ANIMATION // Do not show the footer if quick settings are fully expanded (except // for the foldable split shade view). See b/201427195 && b/222699879. - qsFullScreen -> VisibilityChange.HIDE_WITH_ANIMATION + qsFullScreen -> VisibilityChange.DISAPPEAR_WITH_ANIMATION // Hide the footer if remote input is active (i.e. user is replying to a // notification). See b/75984847. - isRemoteInputActive -> VisibilityChange.HIDE_WITH_ANIMATION - // Never show the footer if the shade is collapsed (e.g. when HUNing). - isShadeClosed -> VisibilityChange.HIDE_WITHOUT_ANIMATION - else -> VisibilityChange.SHOW_WITH_ANIMATION + isRemoteInputActive -> VisibilityChange.DISAPPEAR_WITH_ANIMATION + else -> VisibilityChange.APPEAR_WITH_ANIMATION } } .flowOn(bgDispatcher) @@ -174,9 +200,9 @@ constructor( } enum class VisibilityChange(val visible: Boolean, val canAnimate: Boolean) { - HIDE_WITHOUT_ANIMATION(visible = false, canAnimate = false), - HIDE_WITH_ANIMATION(visible = false, canAnimate = true), - SHOW_WITH_ANIMATION(visible = true, canAnimate = true) + DISAPPEAR_WITHOUT_ANIMATION(visible = false, canAnimate = false), + DISAPPEAR_WITH_ANIMATION(visible = false, canAnimate = true), + APPEAR_WITH_ANIMATION(visible = true, canAnimate = true) } // TODO(b/308591475): This should be tracked separately by the empty shade. @@ -212,4 +238,41 @@ constructor( activeNotificationsInteractor.hasNonClearableSilentNotifications } } + + val topHeadsUpRow: Flow<HeadsUpRowKey?> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(null) + } else { + headsUpNotificationInteractor.topHeadsUpRow + } + } + + val pinnedHeadsUpRows: Flow<Set<HeadsUpRowKey>> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(emptySet()) + } else { + headsUpNotificationInteractor.pinnedHeadsUpRows + } + } + + val headsUpAnimationsEnabled: Flow<Boolean> by lazy { + combine(keyguardInteractor.isKeyguardShowing, shadeInteractor.isShadeFullyExpanded) { + (isKeyguardShowing, isShadeFullyExpanded) -> + // TODO(b/325936094) use isShadeFullyCollapsed instead + !isKeyguardShowing && !isShadeFullyExpanded + } + } + + val hasPinnedHeadsUpRow: Flow<Boolean> by lazy { + if (NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode()) { + flowOf(false) + } else { + headsUpNotificationInteractor.hasPinnedRows + } + } + + // TODO(b/325936094) use it for the text displayed in the StatusBar + fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowViewModel = + HeadsUpRowViewModel(headsUpNotificationInteractor.headsUpRow(key)) + fun elementKeyFor(key: HeadsUpRowKey): Any = headsUpNotificationInteractor.elementKeyFor(key) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt index b6167e1ef0fb..a7cbc3374a0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.compose.animation.scene.ObservableTransitionState -import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager @@ -27,6 +26,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.Scenes.Shade import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor +import com.android.systemui.statusbar.notification.stack.shared.model.StackClipping import com.android.systemui.util.kotlin.FlowDumperImpl import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -83,8 +83,13 @@ constructor( .dumpWhileCollecting("expandFraction") /** The bounds of the notification stack in the current scene. */ - val stackBounds: Flow<NotificationContainerBounds> = - stackAppearanceInteractor.stackBounds.dumpValue("stackBounds") + val stackClipping: Flow<StackClipping> = + combine( + stackAppearanceInteractor.stackBounds, + stackAppearanceInteractor.stackRounding, + ::StackClipping + ) + .dumpWhileCollecting("stackClipping") /** The y-coordinate in px of top of the contents of the notification stack. */ val contentTop: StateFlow<Float> = stackAppearanceInteractor.contentTop.dumpValue("contentTop") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 9e2497d5bb41..bd83121d9a34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -24,6 +24,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor +import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds +import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding import javax.inject.Inject import kotlinx.coroutines.flow.Flow @@ -61,12 +63,17 @@ constructor( right: Float, bottom: Float, ) { - val notificationContainerBounds = - NotificationContainerBounds(top = top, bottom = bottom, left = left, right = right) - keyguardInteractor.setNotificationContainerBounds(notificationContainerBounds) - interactor.setStackBounds(notificationContainerBounds) + keyguardInteractor.setNotificationContainerBounds( + NotificationContainerBounds(top = top, bottom = bottom) + ) + interactor.setStackBounds( + StackBounds(top = top, bottom = bottom, left = left, right = right) + ) } + /** Corner rounding of the stack */ + val stackRounding: Flow<StackRounding> = interactor.stackRounding + /** * The height in px of the contents of notification stack. Depending on the number of * notifications, this can exceed the space available on screen to show notifications, at which diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index a38840b10b5f..d112edb9772c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -35,7 +35,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED -import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel @@ -366,27 +366,40 @@ constructor( } } } - .onStart { emit(0f) } + .onStart { emit(1f) } .dumpWhileCollecting("alphaForShadeAndQsExpansion") - private val alphaWhenGoneAndShadeState: Flow<Float> = - combineTransform( - keyguardTransitionInteractor.transitions - .map { step -> step.to == GONE && step.transitionState == FINISHED } - .distinctUntilChanged(), - keyguardInteractor.statusBarState, - ) { isGoneTransitionFinished, statusBarState -> - if (isGoneTransitionFinished && statusBarState == SHADE) { - emit(1f) + private val isGoneTransitionRunning: Flow<Boolean> = + flow { + while (currentCoroutineContext().isActive) { + emit(false) + // Ensure start where GONE is inactive + keyguardTransitionInteractor.transitionValue(GONE).first { it == 0f } + // Wait for a GONE transition to begin + keyguardTransitionInteractor.transitionStepsToState(GONE).first { + it.value > 0f && it.transitionState == RUNNING + } + emit(true) + // Now await the signal that SHADE state has been reached or the GONE transition + // was reversed. Until SHADE state has been replaced and merged with GONE, it is + // the only source of when it is considered safe to reset alpha to 1f for HUNs. + combine( + keyguardInteractor.statusBarState, + // Emit -1f on start to make sure the flow runs + keyguardTransitionInteractor.transitionValue(GONE).onStart { emit(-1f) } + ) { statusBarState, goneValue -> + statusBarState == SHADE || goneValue == 0f + } + .first { it } } } - .dumpWhileCollecting("alphaWhenGoneAndShadeState") + .dumpWhileCollecting("goneTransitionInProgress") fun keyguardAlpha(viewState: ViewStateAccessor): Flow<Float> { // All transition view models are mututally exclusive, and safe to merge val alphaTransitions = merge( - alternateBouncerToGoneTransitionViewModel.lockscreenAlpha, + alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState), aodToLockscreenTransitionViewModel.notificationAlpha, aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToLockscreenTransitionViewModel.lockscreenAlpha, @@ -407,12 +420,11 @@ constructor( return merge( alphaTransitions, - // Sends a final alpha value of 1f when truly gone, to make sure HUNs appear - alphaWhenGoneAndShadeState, // These remaining cases handle alpha changes within an existing state, such as // shade expansion or swipe to dismiss combineTransform( isOnLockscreenWithoutShade, + isGoneTransitionRunning, shadeCollapseFadeIn, alphaForShadeAndQsExpansion, keyguardInteractor.dismissAlpha.dumpWhileCollecting( @@ -420,6 +432,7 @@ constructor( ), ) { isOnLockscreenWithoutShade, + isGoneTransitionRunning, shadeCollapseFadeIn, alphaForShadeAndQsExpansion, dismissAlpha -> @@ -427,7 +440,7 @@ constructor( if (!shadeCollapseFadeIn && dismissAlpha != null) { emit(dismissAlpha) } - } else { + } else if (!isGoneTransitionRunning) { emit(alphaForShadeAndQsExpansion) } }, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt new file mode 100644 index 000000000000..cb360fed77bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinder.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.ui.viewbinder + +import android.util.Log +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.shared.HeadsUpRowKey +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch + +private const val TAG = "HunBinder" +private val DEBUG = true // Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG) + +class HeadsUpNotificationViewBinder +@Inject +constructor(private val viewModel: NotificationListViewModel) { + suspend fun bindHeadsUpNotifications(parentView: NotificationStackScrollLayout): Unit = + coroutineScope { + launch { + var previousKeys = emptySet<HeadsUpRowKey>() + viewModel.pinnedHeadsUpRows + .sample(viewModel.headsUpAnimationsEnabled, ::Pair) + .collect { (newKeys, animationsEnabled) -> + if (DEBUG) { + Log.d(TAG, "update:$newKeys") + } + + val added = newKeys - previousKeys + val removed = previousKeys - newKeys + previousKeys = newKeys + + if (animationsEnabled) { + added.forEach { key -> + parentView.generateHeadsUpAnimation( + obtainView(key), + /* isHeadsUp = */ true + ) + } + removed.forEach { key -> + val row = obtainView(key) + parentView.generateHeadsUpAnimation(row, /* isHeadsUp = */ false) + row.setHeadsUpIsVisible() + } + } + } + } + launch { + viewModel.topHeadsUpRow.collect { key -> + parentView.setTopHeadsUpRow(key?.let(::obtainView)) + } + } + launch { + viewModel.hasPinnedHeadsUpRow.collect { parentView.setInHeadsUpPinnedMode(it) } + } + } + + private fun obtainView(key: HeadsUpRowKey): ExpandableNotificationRow { + return viewModel.elementKeyFor(key) as ExpandableNotificationRow + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt index a55de251314f..37646aea86e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -21,6 +21,7 @@ import android.app.PendingIntent import android.app.TaskStackBuilder import android.content.Context import android.content.Intent +import android.os.Bundle import android.os.RemoteException import android.os.UserHandle import android.provider.Settings @@ -149,6 +150,23 @@ constructor( ) } + override fun startPendingIntentMaybeDismissingKeyguard( + intent: PendingIntent, + intentSentUiThreadCallback: Runnable?, + animationController: ActivityTransitionAnimator.Controller?, + fillInIntent: Intent?, + extraOptions: Bundle?, + ) { + activityStarterInternal.startPendingIntentDismissingKeyguard( + intent = intent, + intentSentUiThreadCallback = intentSentUiThreadCallback, + animationController = animationController, + showOverLockscreen = true, + fillInIntent = fillInIntent, + extraOptions = extraOptions, + ) + } + /** * TODO(b/279084380): Change callers to just call startActivityDismissingKeyguard and deprecate * this. @@ -554,6 +572,8 @@ constructor( associatedView: View? = null, animationController: ActivityTransitionAnimator.Controller? = null, showOverLockscreen: Boolean = false, + fillInIntent: Intent? = null, + extraOptions: Bundle? = null, ) { val animationController = if (associatedView is ExpandableNotificationRow) { @@ -614,9 +634,10 @@ constructor( val options = ActivityOptions( CentralSurfaces.getActivityOptions( - displayId, - animationAdapter - ) + displayId, + animationAdapter + ) + .apply { extraOptions?.let { putAll(it) } } ) // TODO b/221255671: restrict this to only be set for // notifications @@ -625,9 +646,9 @@ constructor( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED ) return intent.sendAndReturnResult( - null, + context, 0, - null, + fillInIntent, null, null, null, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 0db5c64c4c4e..665fc0aab316 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -537,7 +537,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba @VisibleForTesting void vibrateOnNavigationKeyDown() { - mShadeViewController.performHapticFeedback( + mShadeController.performHapticFeedback( HapticFeedbackConstants.GESTURE_START ); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index d32e88b79776..f76de04c0c18 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -27,7 +27,6 @@ import static androidx.lifecycle.Lifecycle.State.RESUMED; import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME; import static com.android.systemui.Flags.lightRevealMigration; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.Flags.newAodTransition; import static com.android.systemui.Flags.predictiveBackSysui; import static com.android.systemui.Flags.truncatedStatusBarIconsFix; @@ -142,6 +141,7 @@ import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder; @@ -1470,7 +1470,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return (v, event) -> { mAutoHideController.checkUserAutoHide(event); mRemoteInputManager.checkRemoteInputOutside(event); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mShadeController.onStatusBarTouch(event); } return getNotificationShadeWindowView().onTouchEvent(event); @@ -2507,7 +2507,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> { mDeviceInteractive = true; - boolean isFlaggedOff = newAodTransition() && migrateClocksToBlueprint(); + boolean isFlaggedOff = newAodTransition() && MigrateClocksToBlueprint.isEnabled(); if (!isFlaggedOff && shouldAnimateDozeWakeup()) { // If this is false, the power button must be physically pressed in order to // trigger fingerprint authentication. @@ -3147,7 +3147,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { public void onDozeAmountChanged(float linear, float eased) { if (!lightRevealMigration() && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) { - mLightRevealScrim.setRevealAmount(1f - linear); + if (DeviceEntryUdfpsRefactor.isEnabled()) { + // If wakeAndUnlocking, this is handled in AuthRippleInteractor + if (!mBiometricUnlockController.isWakeAndUnlock()) { + mLightRevealScrim.setRevealAmount(1f - linear); + } + } else { + mLightRevealScrim.setRevealAmount(1f - linear); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java index 442e43a9dae2..7abcf1337602 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java @@ -41,7 +41,7 @@ import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.domain.interactor.DozeInteractor; import com.android.systemui.shade.NotificationShadeWindowViewController; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.StatusBarState; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index 24be3db6231f..3f200d578261 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -22,6 +22,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Region; import android.os.Handler; +import android.util.ArrayMap; import android.util.Pools; import androidx.collection.ArraySet; @@ -40,7 +41,10 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.OnReorderingAllowedListener; import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; +import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository; +import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.AnimationStateHandler; import com.android.systemui.statusbar.policy.AvalancheController; @@ -58,13 +62,21 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.Stack; import javax.inject.Inject; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.MutableStateFlow; +import kotlinx.coroutines.flow.StateFlow; +import kotlinx.coroutines.flow.StateFlowKt; + /** A implementation of HeadsUpManager for phone. */ @SysUISingleton -public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUpChangedListener { +public class HeadsUpManagerPhone extends BaseHeadsUpManager implements + HeadsUpRepository, OnHeadsUpChangedListener { private static final String TAG = "HeadsUpManagerPhone"; @VisibleForTesting @@ -73,15 +85,20 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp private final GroupMembershipManager mGroupMembershipManager; private final List<OnHeadsUpPhoneListenerChange> mHeadsUpPhoneListeners = new ArrayList<>(); private final VisualStabilityProvider mVisualStabilityProvider; - private boolean mReleaseOnExpandFinish; + // TODO(b/328393698) move the topHeadsUpRow logic to an interactor + private final MutableStateFlow<HeadsUpRowRepository> mTopHeadsUpRow = + StateFlowKt.MutableStateFlow(null); + private final MutableStateFlow<Set<HeadsUpRowRepository>> mHeadsUpNotificationRows = + StateFlowKt.MutableStateFlow(new HashSet<>()); + private final MutableStateFlow<Boolean> mHeadsUpGoingAway = StateFlowKt.MutableStateFlow(false); + private boolean mReleaseOnExpandFinish; private boolean mTrackingHeadsUp; private final HashSet<String> mSwipedOutKeys = new HashSet<>(); private final HashSet<NotificationEntry> mEntriesToRemoveAfterExpand = new HashSet<>(); private final ArraySet<NotificationEntry> mEntriesToRemoveWhenReorderingAllowed = new ArraySet<>(); private boolean mIsExpanded; - private boolean mHeadsUpGoingAway; private int mStatusBarState; private AnimationStateHandler mAnimationStateHandler; private int mHeadsUpInset; @@ -94,6 +111,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp @Override public HeadsUpEntryPhone acquire() { + NotificationsHeadsUpRefactor.assertInLegacyMode(); if (!mPoolObjects.isEmpty()) { return mPoolObjects.pop(); } @@ -102,6 +120,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp @Override public boolean release(@NonNull HeadsUpEntryPhone instance) { + NotificationsHeadsUpRefactor.assertInLegacyMode(); mPoolObjects.push(instance); return true; } @@ -245,7 +264,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp if (isExpanded != mIsExpanded) { mIsExpanded = isExpanded; if (isExpanded) { - mHeadsUpGoingAway = false; + mHeadsUpGoingAway.setValue(false); } } } @@ -256,17 +275,17 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp */ @Override public void setHeadsUpGoingAway(boolean headsUpGoingAway) { - if (headsUpGoingAway != mHeadsUpGoingAway) { - mHeadsUpGoingAway = headsUpGoingAway; + if (headsUpGoingAway != mHeadsUpGoingAway.getValue()) { for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) { listener.onHeadsUpGoingAwayStateChanged(headsUpGoingAway); } + mHeadsUpGoingAway.setValue(headsUpGoingAway); } } @Override public boolean isHeadsUpGoingAway() { - return mHeadsUpGoingAway; + return mHeadsUpGoingAway.getValue(); } /** @@ -285,6 +304,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp } else { headsUpEntry.updateEntry(false /* updatePostTime */, "setRemoteInputActive(false)"); } + onEntryUpdated(headsUpEntry); } } @@ -371,15 +391,48 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp /////////////////////////////////////////////////////////////////////////////////////////////// // HeadsUpManager utility (protected) methods overrides: + @NonNull @Override - protected HeadsUpEntry createHeadsUpEntry() { - return mEntryPool.acquire(); + protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { + if (NotificationsHeadsUpRefactor.isEnabled()) { + return new HeadsUpEntryPhone(entry); + } else { + HeadsUpEntryPhone headsUpEntry = mEntryPool.acquire(); + headsUpEntry.setEntry(entry); + return headsUpEntry; + } + } + + @Override + protected void onEntryAdded(HeadsUpEntry headsUpEntry) { + super.onEntryAdded(headsUpEntry); + updateTopHeadsUpFlow(); + updateHeadsUpFlow(); + } + + @Override + protected void onEntryUpdated(HeadsUpEntry headsUpEntry) { + super.onEntryUpdated(headsUpEntry); + // no need to update the list here + updateTopHeadsUpFlow(); } @Override protected void onEntryRemoved(HeadsUpEntry headsUpEntry) { super.onEntryRemoved(headsUpEntry); - mEntryPool.release((HeadsUpEntryPhone) headsUpEntry); + if (!NotificationsHeadsUpRefactor.isEnabled()) { + mEntryPool.release((HeadsUpEntryPhone) headsUpEntry); + } + updateTopHeadsUpFlow(); + updateHeadsUpFlow(); + } + + private void updateTopHeadsUpFlow() { + mTopHeadsUpRow.setValue((HeadsUpRowRepository) getTopHeadsUpEntry()); + } + + private void updateHeadsUpFlow() { + mHeadsUpNotificationRows.setValue(new HashSet<>(getHeadsUpEntryPhoneMap().values())); } @Override @@ -403,6 +456,12 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp /////////////////////////////////////////////////////////////////////////////////////////////// // Private utility methods: + @NonNull + private ArrayMap<String, HeadsUpEntryPhone> getHeadsUpEntryPhoneMap() { + //noinspection unchecked + return (ArrayMap<String, HeadsUpEntryPhone>) ((ArrayMap) mHeadsUpEntryMap); + } + @Nullable private HeadsUpEntryPhone getHeadsUpEntryPhone(@NonNull String key) { return (HeadsUpEntryPhone) mHeadsUpEntryMap.get(key); @@ -410,7 +469,11 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp @Nullable private HeadsUpEntryPhone getTopHeadsUpEntryPhone() { - return (HeadsUpEntryPhone) getTopHeadsUpEntry(); + if (NotificationsHeadsUpRefactor.isEnabled()) { + return (HeadsUpEntryPhone) mTopHeadsUpRow.getValue(); + } else { + return (HeadsUpEntryPhone) getTopHeadsUpEntry(); + } } @Override @@ -427,26 +490,73 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp return headsUpEntry == null || headsUpEntry != topEntry || super.canRemoveImmediately(key); } + @Override + @NonNull + public Flow<HeadsUpRowRepository> getTopHeadsUpRow() { + return mTopHeadsUpRow; + } + + @Override + @NonNull + public Flow<Set<HeadsUpRowRepository>> getActiveHeadsUpRows() { + return mHeadsUpNotificationRows; + } + + @Override + @NonNull + public Flow<Boolean> getHeadsUpAnimatingAway() { + return mHeadsUpGoingAway; + } + /////////////////////////////////////////////////////////////////////////////////////////////// // HeadsUpEntryPhone: - protected class HeadsUpEntryPhone extends BaseHeadsUpManager.HeadsUpEntry { + protected class HeadsUpEntryPhone extends BaseHeadsUpManager.HeadsUpEntry implements + HeadsUpRowRepository { private boolean mGutsShownPinned; + private final MutableStateFlow<Boolean> mIsPinned = StateFlowKt.MutableStateFlow(false); /** * If the time this entry has been on was extended */ private boolean extended; - @Override public boolean isSticky() { return super.isSticky() || mGutsShownPinned; } - public void setEntry(@NonNull final NotificationEntry entry) { - Runnable removeHeadsUpRunnable = () -> { + public HeadsUpEntryPhone() { + super(); + } + + public HeadsUpEntryPhone(NotificationEntry entry) { + super(entry); + } + + @Override + @NonNull + public String getKey() { + return requireEntry().getKey(); + } + + @Override + @NonNull + public StateFlow<Boolean> isPinned() { + return mIsPinned; + } + + @Override + protected void setRowPinned(boolean pinned) { + // TODO(b/327624082): replace this super call with a ViewBinder + super.setRowPinned(pinned); + mIsPinned.setValue(pinned); + } + + @Override + protected Runnable createRemoveRunnable(NotificationEntry entry) { + return () -> { if (!mVisualStabilityProvider.isReorderingAllowed() // We don't want to allow reordering while pulsing, but headsup need to // time out anyway @@ -460,8 +570,6 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp removeEntry(entry.getKey()); } }; - - setEntry(entry, removeHeadsUpRunnable); } @Override @@ -521,6 +629,17 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp protected long calculateFinishTime() { return super.calculateFinishTime() + (extended ? mExtensionTime : 0); } + + @Override + @NonNull + public Object getElementKey() { + return requireEntry().getRow(); + } + + private NotificationEntry requireEntry() { + /* check if */ NotificationsHeadsUpRefactor.isUnexpectedlyInLegacyMode(); + return Objects.requireNonNull(mEntry); + } } private final StateListener mStatusBarStateListener = new StateListener() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java index 94f62e075a4a..f84efbbf9293 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.Flags.newAodTransition; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import android.content.Context; import android.content.res.Resources; @@ -41,6 +40,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -545,7 +545,7 @@ public class LegacyNotificationIconAreaControllerImpl implements return; } if (mScreenOffAnimationController.shouldAnimateAodIcons()) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mAodIcons.setTranslationY(-mAodIconAppearTranslation); } mAodIcons.setAlpha(0); @@ -557,14 +557,14 @@ public class LegacyNotificationIconAreaControllerImpl implements .start(); } else { mAodIcons.setAlpha(1.0f); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mAodIcons.setTranslationY(0); } } } private void animateInAodIconTranslation() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mAodIcons.animate() .setInterpolator(Interpolators.DECELERATE_QUINT) .translationY(0) @@ -667,7 +667,7 @@ public class LegacyNotificationIconAreaControllerImpl implements } } else { mAodIcons.setAlpha(1.0f); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mAodIcons.setTranslationY(0); } mAodIcons.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index f99817aa4aad..a99834ad3456 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -89,7 +89,7 @@ import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.shade.ShadeExpansionListener; import com.android.systemui.shade.ShadeExpansionStateManager; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.NotificationShadeWindowController; @@ -106,6 +106,8 @@ import com.android.systemui.util.kotlin.JavaAdapter; import dagger.Lazy; +import kotlin.Unit; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashSet; @@ -115,7 +117,6 @@ import java.util.Set; import javax.inject.Inject; -import kotlin.Unit; import kotlinx.coroutines.CoroutineDispatcher; import kotlinx.coroutines.ExperimentalCoroutinesApi; import kotlinx.coroutines.Job; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 67d2299a9a3d..479aef167b5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -19,10 +19,11 @@ import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.systemui.DejankUtils import com.android.systemui.Flags.lightRevealMigration -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.statusbar.CircleReveal @@ -45,9 +46,7 @@ import javax.inject.Inject */ private const val ANIMATE_IN_KEYGUARD_DELAY = 600L -/** - * Duration for the light reveal portion of the animation. - */ +/** Duration for the light reveal portion of the animation. */ private const val LIGHT_REVEAL_ANIMATION_DURATION = 500L /** @@ -58,7 +57,9 @@ private const val LIGHT_REVEAL_ANIMATION_DURATION = 500L * and then animates in the AOD UI. */ @SysUISingleton -class UnlockedScreenOffAnimationController @Inject constructor( +class UnlockedScreenOffAnimationController +@Inject +constructor( private val context: Context, private val wakefulnessLifecycle: WakefulnessLifecycle, private val statusBarStateControllerImpl: StatusBarStateControllerImpl, @@ -69,11 +70,11 @@ class UnlockedScreenOffAnimationController @Inject constructor( private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>, private val interactionJankMonitor: InteractionJankMonitor, private val powerManager: PowerManager, + private val shadeLockscreenInteractorLazy: Lazy<ShadeLockscreenInteractor>, private val panelExpansionInteractorLazy: Lazy<PanelExpansionInteractor>, private val handler: Handler = Handler(), ) : WakefulnessLifecycle.Observer, ScreenOffAnimation { private lateinit var centralSurfaces: CentralSurfaces - private lateinit var shadeViewController: ShadeViewController /** * Whether or not [initialize] has been called to provide us with the StatusBar, * NotificationPanelViewController, and LightRevealSrim so that we can run the unlocked screen @@ -95,52 +96,61 @@ class UnlockedScreenOffAnimationController @Inject constructor( */ private var decidedToAnimateGoingToSleep: Boolean? = null - private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply { - duration = LIGHT_REVEAL_ANIMATION_DURATION - interpolator = Interpolators.LINEAR - addUpdateListener { - if (lightRevealMigration()) return@addUpdateListener - if (lightRevealScrim.revealEffect !is CircleReveal) { - lightRevealScrim.revealAmount = it.animatedValue as Float - } - if (lightRevealScrim.isScrimAlmostOccludes && - interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF)) { - // ends the instrument when the scrim almost occludes the screen. - // because the following janky frames might not be perceptible. - interactionJankMonitor.end(CUJ_SCREEN_OFF) - } - } - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationCancel(animation: Animator) { - if (lightRevealMigration()) return + private val lightRevealAnimator = + ValueAnimator.ofFloat(1f, 0f).apply { + duration = LIGHT_REVEAL_ANIMATION_DURATION + interpolator = Interpolators.LINEAR + addUpdateListener { + if (lightRevealMigration()) return@addUpdateListener if (lightRevealScrim.revealEffect !is CircleReveal) { - lightRevealScrim.revealAmount = 1f + lightRevealScrim.revealAmount = it.animatedValue as Float + } + if ( + lightRevealScrim.isScrimAlmostOccludes && + interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF) + ) { + // ends the instrument when the scrim almost occludes the screen. + // because the following janky frames might not be perceptible. + interactionJankMonitor.end(CUJ_SCREEN_OFF) } } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationCancel(animation: Animator) { + if (lightRevealMigration()) return + if (lightRevealScrim.revealEffect !is CircleReveal) { + lightRevealScrim.revealAmount = 1f + } + } - override fun onAnimationEnd(animation: Animator) { - lightRevealAnimationPlaying = false - interactionJankMonitor.end(CUJ_SCREEN_OFF) - } + override fun onAnimationEnd(animation: Animator) { + lightRevealAnimationPlaying = false + interactionJankMonitor.end(CUJ_SCREEN_OFF) + } - override fun onAnimationStart(animation: Animator) { - interactionJankMonitor.begin( - notifShadeWindowControllerLazy.get().windowRootView, CUJ_SCREEN_OFF) - } - }) - } + override fun onAnimationStart(animation: Animator) { + interactionJankMonitor.begin( + notifShadeWindowControllerLazy.get().windowRootView, + CUJ_SCREEN_OFF + ) + } + } + ) + } // FrameCallback used to delay starting the light reveal animation until the next frame - private val startLightRevealCallback = namedRunnable("startLightReveal") { - lightRevealAnimationPlaying = true - lightRevealAnimator.start() - } + private val startLightRevealCallback = + namedRunnable("startLightReveal") { + lightRevealAnimationPlaying = true + lightRevealAnimator.start() + } - private val animatorDurationScaleObserver = object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - updateAnimatorDurationScale() + private val animatorDurationScaleObserver = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + updateAnimatorDurationScale() + } } - } override fun initialize( centralSurfaces: CentralSurfaces, @@ -150,26 +160,24 @@ class UnlockedScreenOffAnimationController @Inject constructor( this.initialized = true this.lightRevealScrim = lightRevealScrim this.centralSurfaces = centralSurfaces - this.shadeViewController = shadeViewController updateAnimatorDurationScale() globalSettings.registerContentObserver( - Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), - /* notify for descendants */ false, - animatorDurationScaleObserver) + Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), + /* notify for descendants */ false, + animatorDurationScaleObserver + ) wakefulnessLifecycle.addObserver(this) } fun updateAnimatorDurationScale() { - animatorDurationScale = fixScale( - globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f)) + animatorDurationScale = + fixScale(globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f)) } - override fun shouldDelayKeyguardShow(): Boolean = - shouldPlayAnimation() + override fun shouldDelayKeyguardShow(): Boolean = shouldPlayAnimation() - override fun isKeyguardShowDelayed(): Boolean = - isAnimationPlaying() + override fun isKeyguardShowDelayed(): Boolean = isAnimationPlaying() /** * Animates in the provided keyguard view, ending in the same position that it will be in on @@ -190,15 +198,21 @@ class UnlockedScreenOffAnimationController @Inject constructor( // We animate the Y properly separately using the PropertyAnimator, as the panel // view also needs to update the end position. PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y) - PropertyAnimator.setProperty(keyguardView, AnimatableProperty.Y, currentY, - AnimationProperties().setDuration(duration.toLong()), - true /* animate */) + PropertyAnimator.setProperty( + keyguardView, + AnimatableProperty.Y, + currentY, + AnimationProperties().setDuration(duration.toLong()), + true /* animate */ + ) // Cancel any existing CUJs before starting the animation interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA) PropertyAnimator.setProperty( - keyguardView, AnimatableProperty.ALPHA, 1f, + keyguardView, + AnimatableProperty.ALPHA, + 1f, AnimationProperties() .setDelay(0) .setDuration(duration.toLong()) @@ -230,13 +244,14 @@ class UnlockedScreenOffAnimationController @Inject constructor( interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) } .setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN), - true /* animate */) - val builder = InteractionJankMonitor.Configuration.Builder - .withView( + true /* animate */ + ) + val builder = + InteractionJankMonitor.Configuration.Builder.withView( InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD, checkNotNull(notifShadeWindowControllerLazy.get().windowRootView) - ) - .setTag(statusBarStateControllerImpl.getClockId()) + ) + .setTag(statusBarStateControllerImpl.getClockId()) interactionJankMonitor.begin(builder) } @@ -284,25 +299,34 @@ class UnlockedScreenOffAnimationController @Inject constructor( // chance of missing the first frame, so to mitigate this we should start the animation // on the next frame. DejankUtils.postAfterTraversal(startLightRevealCallback) - handler.postDelayed({ - // Only run this callback if the device is sleeping (not interactive). This callback - // is removed in onStartedWakingUp, but since that event is asynchronously - // dispatched, a race condition could make it possible for this callback to be run - // as the device is waking up. That results in the AOD UI being shown while we wake - // up, with unpredictable consequences. - if (!powerManager.isInteractive(Display.DEFAULT_DISPLAY) && - shouldAnimateInKeyguard) { - if (!migrateClocksToBlueprint()) { - // Tracking this state should no longer be relevant, as the isInteractive - // check covers it - aodUiAnimationPlaying = true + handler.postDelayed( + { + // Only run this callback if the device is sleeping (not interactive). This + // callback + // is removed in onStartedWakingUp, but since that event is asynchronously + // dispatched, a race condition could make it possible for this callback to be + // run + // as the device is waking up. That results in the AOD UI being shown while we + // wake + // up, with unpredictable consequences. + if ( + !powerManager.isInteractive(Display.DEFAULT_DISPLAY) && + shouldAnimateInKeyguard + ) { + if (!MigrateClocksToBlueprint.isEnabled) { + // Tracking this state should no longer be relevant, as the + // isInteractive + // check covers it + aodUiAnimationPlaying = true + } + + // Show AOD. That'll cause the KeyguardVisibilityHelper to call + // #animateInKeyguard. + shadeLockscreenInteractorLazy.get().showAodUi() } - - // Show AOD. That'll cause the KeyguardVisibilityHelper to call - // #animateInKeyguard. - shadeViewController.showAodUi() - } - }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong()) + }, + (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong() + ) return true } else { @@ -335,8 +359,12 @@ class UnlockedScreenOffAnimationController @Inject constructor( } // If animations are disabled system-wide, don't play this one either. - if (Settings.Global.getString( - context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE) == "0") { + if ( + Settings.Global.getString( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE + ) == "0" + ) { return false } @@ -360,8 +388,10 @@ class UnlockedScreenOffAnimationController @Inject constructor( // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree // portrait. If we're in another orientation, disable the screen off animation so we don't // animate in the keyguard AOD UI sideways or upside down. - if (!keyguardStateController.isKeyguardScreenRotationAllowed && - context.display?.rotation != Surface.ROTATION_0) { + if ( + !keyguardStateController.isKeyguardScreenRotationAllowed && + context.display?.rotation != Surface.ROTATION_0 + ) { return false } @@ -380,23 +410,18 @@ class UnlockedScreenOffAnimationController @Inject constructor( return isScreenOffLightRevealAnimationPlaying() || aodUiAnimationPlaying } - override fun shouldAnimateInKeyguard(): Boolean = - shouldAnimateInKeyguard + override fun shouldAnimateInKeyguard(): Boolean = shouldAnimateInKeyguard - override fun shouldHideScrimOnWakeUp(): Boolean = - isScreenOffLightRevealAnimationPlaying() + override fun shouldHideScrimOnWakeUp(): Boolean = isScreenOffLightRevealAnimationPlaying() override fun overrideNotificationsDozeAmount(): Boolean = shouldPlayUnlockedScreenOffAnimation() && isAnimationPlaying() - override fun shouldShowAodIconsWhenShade(): Boolean = - isAnimationPlaying() + override fun shouldShowAodIconsWhenShade(): Boolean = isAnimationPlaying() - override fun shouldAnimateAodIcons(): Boolean = - shouldPlayUnlockedScreenOffAnimation() + override fun shouldAnimateAodIcons(): Boolean = shouldPlayUnlockedScreenOffAnimation() - override fun shouldPlayAnimation(): Boolean = - shouldPlayUnlockedScreenOffAnimation() + override fun shouldPlayAnimation(): Boolean = shouldPlayUnlockedScreenOffAnimation() /** * Whether the light reveal animation is playing. The second part of the screen off animation, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt index 60b8599ecabd..b085d8046b12 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -301,7 +301,7 @@ class FullMobileConnectionRepository( .flatMapLatest { it.networkName } .logDiffsForTable( tableLogBuffer, - columnPrefix = "", + columnPrefix = "intent", initialValue = activeRepo.value.networkName.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value) @@ -311,7 +311,7 @@ class FullMobileConnectionRepository( .flatMapLatest { it.carrierName } .logDiffsForTable( tableLogBuffer, - columnPrefix = "", + columnPrefix = "sub", initialValue = activeRepo.value.carrierName.value, ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.carrierName.value) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index f01ac0e0a677..5ab2ae899370 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -358,7 +358,13 @@ class MobileConnectionRepositoryImpl( } .stateIn(scope, SharingStarted.WhileSubscribed(), telephonyManager.simCarrierId) - /** BroadcastDispatcher does not handle sticky broadcasts, so we can't use it here */ + /** + * BroadcastDispatcher does not handle sticky broadcasts, so we can't use it here. Note that we + * now use the [SharingStarted.Eagerly] strategy, because there have been cases where the sticky + * broadcast does not represent the correct state. + * + * See b/322432056 for context. + */ @SuppressLint("RegisterReceiverViaContext") override val networkName: StateFlow<NetworkNameModel> = conflatedCallbackFlow { @@ -388,7 +394,7 @@ class MobileConnectionRepositoryImpl( awaitClose { context.unregisterReceiver(receiver) } } .flowOn(bgDispatcher) - .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName) + .stateIn(scope, SharingStarted.Eagerly, defaultNetworkName) override val dataEnabled = run { val initial = telephonyManager.isDataConnectionAllowed diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java index 50de3cba6b59..20a82a403eb7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java @@ -39,6 +39,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.util.ListenerSet; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.settings.GlobalSettings; @@ -162,11 +163,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ @Override public void showNotification(@NonNull NotificationEntry entry) { - HeadsUpEntry headsUpEntry = createHeadsUpEntry(); - - // Attach NotificationEntry for AvalancheController to log key and - // record mPostTime for AvalancheController sorting - headsUpEntry.setEntry(entry); + HeadsUpEntry headsUpEntry = createHeadsUpEntry(entry); Runnable runnable = () -> { // TODO(b/315362456) log outside runnable too @@ -175,6 +172,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { // Add new entry and begin managing it mHeadsUpEntryMap.put(entry.getKey(), headsUpEntry); onEntryAdded(headsUpEntry); + // TODO(b/328390331) move accessibility events to the view layer entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); entry.setIsHeadsUpEntry(true); @@ -235,7 +233,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { // with the groupmanager return; } - + // TODO(b/328390331) move accessibility events to the view layer headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); if (shouldHeadsUpAgain) { @@ -335,15 +333,15 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { if (!isPinned) { headsUpEntry.mWasUnpinned = true; } - if (headsUpEntry.isPinned() != isPinned) { - headsUpEntry.setPinned(isPinned); + if (headsUpEntry.isRowPinned() != isPinned) { + headsUpEntry.setRowPinned(isPinned); updatePinnedMode(); if (isPinned && entry.getSbn() != null) { mUiEventLogger.logWithInstanceId( NotificationPeekEvent.NOTIFICATION_PEEK, entry.getSbn().getUid(), entry.getSbn().getPackageName(), entry.getSbn().getInstanceId()); } - // TODO(b/325936094) convert these listeners to collecting a flow + // TODO(b/325936094) use the isPinned Flow instead for (OnHeadsUpChangedListener listener : mListeners) { if (isPinned) { listener.onHeadsUpPinned(entry); @@ -362,7 +360,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { * Manager-specific logic that should occur when an entry is added. * @param headsUpEntry entry added */ - void onEntryAdded(HeadsUpEntry headsUpEntry) { + protected void onEntryAdded(HeadsUpEntry headsUpEntry) { NotificationEntry entry = headsUpEntry.mEntry; entry.setHeadsUp(true); @@ -375,7 +373,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } /** - * Remove a notification and reset the entry. + * Remove a notification from the alerting entries. * @param key key of notification to remove */ protected final void removeEntry(@NonNull String key) { @@ -394,8 +392,13 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { entry.demoteStickyHun(); mHeadsUpEntryMap.remove(key); onEntryRemoved(headsUpEntry); + // TODO(b/328390331) move accessibility events to the view layer entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - headsUpEntry.reset(); + if (NotificationsHeadsUpRefactor.isEnabled()) { + headsUpEntry.cancelAutoRemovalCallbacks("removeEntry"); + } else { + headsUpEntry.reset(); + } }; mAvalancheController.delete(headsUpEntry, runnable, "removeEntry"); } @@ -415,7 +418,16 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } } - private void updatePinnedMode() { + /** + * Manager-specific logic, that should occur, when the entry is updated, and its posted time has + * changed. + * + * @param headsUpEntry entry updated + */ + protected void onEntryUpdated(HeadsUpEntry headsUpEntry) { + } + + protected void updatePinnedMode() { boolean hasPinnedNotification = hasPinnedNotificationInternal(); if (hasPinnedNotification == mHasPinnedNotification) { return; @@ -470,7 +482,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { @Nullable protected HeadsUpEntry getHeadsUpEntry(@NonNull String key) { // TODO(b/315362456) See if callers need to check AvalancheController - return (HeadsUpEntry) mHeadsUpEntryMap.get(key); + return mHeadsUpEntryMap.get(key); } /** @@ -490,7 +502,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { HeadsUpEntry topEntry = null; for (HeadsUpEntry entry: mHeadsUpEntryMap.values()) { if (topEntry == null || entry.compareTo(topEntry) < 0) { - topEntry = (HeadsUpEntry) entry; + topEntry = entry; } } return topEntry; @@ -657,8 +669,8 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } @NonNull - protected HeadsUpEntry createHeadsUpEntry() { - return new HeadsUpEntry(); + protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { + return new HeadsUpEntry(entry); } /** @@ -694,11 +706,23 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { @Nullable private Runnable mCancelRemoveRunnable; + public HeadsUpEntry() { + NotificationsHeadsUpRefactor.assertInLegacyMode(); + } + + public HeadsUpEntry(NotificationEntry entry) { + // Attach NotificationEntry for AvalancheController to log key and + // record mPostTime for AvalancheController sorting + setEntry(entry, createRemoveRunnable(entry)); + } + + /** Attach a NotificationEntry. */ public void setEntry(@NonNull final NotificationEntry entry) { - setEntry(entry, () -> removeEntry(entry.getKey())); + NotificationsHeadsUpRefactor.assertInLegacyMode(); + setEntry(entry, createRemoveRunnable(entry)); } - public void setEntry(@NonNull final NotificationEntry entry, + private void setEntry(@NonNull final NotificationEntry entry, @Nullable Runnable removeRunnable) { mEntry = entry; mRemoveRunnable = removeRunnable; @@ -707,11 +731,11 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { updateEntry(true /* updatePostTime */, "setEntry"); } - public boolean isPinned() { + protected boolean isRowPinned() { return mEntry != null && mEntry.isRowPinned(); } - public void setPinned(boolean pinned) { + protected void setRowPinned(boolean pinned) { if (mEntry != null) mEntry.setRowPinned(pinned); } @@ -751,6 +775,9 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { return timeLeft; }; scheduleAutoRemovalCallback(finishTimeCalculator, "updateEntry (not sticky)"); + + // Notify the manager, that the posted time has changed. + onEntryUpdated(this); } /** @@ -847,6 +874,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } public void reset() { + NotificationsHeadsUpRefactor.assertInLegacyMode(); cancelAutoRemovalCallbacks("reset()"); mEntry = null; mRemoveRunnable = null; @@ -919,6 +947,11 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } } + /** Creates a runnable to remove this notification from the alerting entries. */ + protected Runnable createRemoveRunnable(NotificationEntry entry) { + return () -> removeEntry(entry.getKey()); + } + /** * Calculate what the post time of a notification is at some current time. * @return the post time diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt index 420701f026d2..52a2e9ccc163 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt @@ -196,6 +196,7 @@ interface OnHeadsUpPhoneListenerChange { * Called when a heads up notification is 'going away' or no longer 'going away'. See * [HeadsUpManager.setHeadsUpGoingAway]. */ + // TODO(b/325936094) delete this callback, and listen to the flow instead fun onHeadsUpGoingAwayStateChanged(headsUpGoingAway: Boolean) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt index 087e100e9b33..7a570275d868 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt @@ -42,6 +42,10 @@ import com.android.systemui.qs.tiles.impl.uimodenight.domain.UiModeNightTileMapp import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileDataInteractor import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileUserActionInteractor import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel +import com.android.systemui.qs.tiles.impl.work.domain.interactor.WorkModeTileDataInteractor +import com.android.systemui.qs.tiles.impl.work.domain.interactor.WorkModeTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.qs.tiles.impl.work.ui.WorkModeTileMapper import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel @@ -69,6 +73,7 @@ interface PolicyModule { const val LOCATION_TILE_SPEC = "location" const val ALARM_TILE_SPEC = "alarm" const val UIMODENIGHT_TILE_SPEC = "dark" + const val WORK_MODE_TILE_SPEC = "work" /** Inject flashlight config */ @Provides @@ -197,6 +202,38 @@ interface PolicyModule { stateInteractor, mapper, ) + + /** Inject work mode tile config */ + @Provides + @IntoMap + @StringKey(WORK_MODE_TILE_SPEC) + fun provideWorkModeTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(WORK_MODE_TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status, + labelRes = R.string.quick_settings_work_mode_label, + ), + instanceId = uiEventLogger.getNewInstanceId(), + ) + + /** Inject work mode into tileViewModelMap in QSModule */ + @Provides + @IntoMap + @StringKey(WORK_MODE_TILE_SPEC) + fun provideWorkModeTileViewModel( + factory: QSTileViewModelFactory.Static<WorkModeTileModel>, + mapper: WorkModeTileMapper, + stateInteractor: WorkModeTileDataInteractor, + userActionInteractor: WorkModeTileUserActionInteractor + ): QSTileViewModel = + factory.create( + TileSpec.create(WORK_MODE_TILE_SPEC), + userActionInteractor, + stateInteractor, + mapper, + ) } /** Inject FlashlightTile into tileMap in QSModule */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java index 18ec68bd89eb..1f4c3cd9a017 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.policy; +import static android.permission.flags.Flags.sensitiveNotificationAppProtection; import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS; import static com.android.server.notification.Flags.screenshareNotificationHiding; @@ -23,6 +24,7 @@ import static com.android.server.notification.Flags.screenshareNotificationHidin import android.annotation.MainThread; import android.app.IActivityManager; import android.content.Context; +import android.content.pm.PackageManager; import android.database.ExecutorContentObserver; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; @@ -33,6 +35,9 @@ import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -52,6 +57,7 @@ public class SensitiveNotificationProtectionControllerImpl implements SensitiveNotificationProtectionController { private static final String LOG_TAG = "SNPC"; private final SensitiveNotificationProtectionControllerLogger mLogger; + private final PackageManager mPackageManager; private final ArraySet<String> mExemptPackages = new ArraySet<>(); private final ListenerSet<Runnable> mListeners = new ListenerSet<>(); private volatile MediaProjectionInfo mProjection; @@ -64,17 +70,7 @@ public class SensitiveNotificationProtectionControllerImpl public void onStart(MediaProjectionInfo info) { Trace.beginSection("SNPC.onProjectionStart"); try { - if (mDisableScreenShareProtections) { - Log.w(LOG_TAG, - "Screen share protections disabled, ignoring projectionstart"); - mLogger.logProjectionStart(false, info.getPackageName()); - return; - } - - // Only enable sensitive content protection if sharing full screen - // Launch cookie only set (non-null) if sharing single app/task - updateProjectionStateAndNotifyListeners( - (info.getLaunchCookie() == null) ? info : null); + updateProjectionStateAndNotifyListeners(info); mLogger.logProjectionStart(isSensitiveStateActive(), info.getPackageName()); } finally { Trace.endSection(); @@ -99,10 +95,12 @@ public class SensitiveNotificationProtectionControllerImpl GlobalSettings settings, MediaProjectionManager mediaProjectionManager, IActivityManager activityManager, + PackageManager packageManager, @Main Handler mainHandler, @Background Executor bgExecutor, SensitiveNotificationProtectionControllerLogger logger) { mLogger = logger; + mPackageManager = packageManager; if (!screenshareNotificationHiding()) { return; @@ -168,7 +166,7 @@ public class SensitiveNotificationProtectionControllerImpl mExemptPackages.addAll(exemptPackages); if (mProjection != null) { - mListeners.forEach(Runnable::run); + updateProjectionStateAndNotifyListeners(mProjection); } } @@ -177,13 +175,13 @@ public class SensitiveNotificationProtectionControllerImpl * listeners */ @MainThread - private void updateProjectionStateAndNotifyListeners(MediaProjectionInfo info) { + private void updateProjectionStateAndNotifyListeners(@Nullable MediaProjectionInfo info) { Assert.isMainThread(); // capture previous state boolean wasSensitive = isSensitiveStateActive(); // update internal state - mProjection = info; + mProjection = getNonExemptProjectionInfo(info); // if either previous or new state is sensitive, notify listeners. if (wasSensitive || isSensitiveStateActive()) { @@ -191,6 +189,36 @@ public class SensitiveNotificationProtectionControllerImpl } } + private MediaProjectionInfo getNonExemptProjectionInfo(@Nullable MediaProjectionInfo info) { + if (mDisableScreenShareProtections) { + Log.w(LOG_TAG, "Screen share protections disabled"); + return null; + } else if (info != null && mExemptPackages.contains(info.getPackageName())) { + Log.w(LOG_TAG, "Screen share protections exempt for package " + info.getPackageName()); + return null; + } else if (info != null && canRecordSensitiveContent(info.getPackageName())) { + Log.w(LOG_TAG, "Screen share protections exempt for package " + info.getPackageName() + + " via permission"); + return null; + } else if (info != null && info.getLaunchCookie() != null) { + // Only enable sensitive content protection if sharing full screen + // Launch cookie only set (non-null) if sharing single app/task + Log.w(LOG_TAG, "Screen share protections exempt for single app screenshare"); + return null; + } + return info; + } + + private boolean canRecordSensitiveContent(@NonNull String packageName) { + // RECORD_SENSITIVE_CONTENT is flagged api on sensitiveNotificationAppProtection + if (sensitiveNotificationAppProtection()) { + return mPackageManager.checkPermission( + android.Manifest.permission.RECORD_SENSITIVE_CONTENT, packageName) + == PackageManager.PERMISSION_GRANTED; + } + return false; + } + @Override public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) { mListeners.addIfAbsent(onSensitiveStateChanged); @@ -201,15 +229,9 @@ public class SensitiveNotificationProtectionControllerImpl mListeners.remove(onSensitiveStateChanged); } - // TODO(b/323396693): opportunity for optimization @Override public boolean isSensitiveStateActive() { - MediaProjectionInfo projection = mProjection; - if (projection == null) { - return false; - } - - return !mExemptPackages.contains(projection.getPackageName()); + return mProjection != null; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt index ac1d2803835a..e977014e00f2 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt @@ -91,7 +91,8 @@ constructor( ) controller.init() - applicationScope.launch(bgHandler.asCoroutineDispatcher()) { + val bgDispatcher = bgHandler.asCoroutineDispatcher("@UnfoldBg Handler") + applicationScope.launch(bgDispatcher) { powerInteractor.screenPowerState.collect { if (it == ScreenPowerState.SCREEN_ON) { readyCallback = null @@ -99,7 +100,7 @@ constructor( } } - applicationScope.launch(bgHandler.asCoroutineDispatcher()) { + applicationScope.launch(bgDispatcher) { deviceStateRepository.state .map { it == DeviceStateRepository.DeviceState.FOLDED } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt new file mode 100644 index 000000000000..de036eaebaa2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import kotlinx.coroutines.DisposableHandle + +/** A mutable collection of [DisposableHandle] objects that is itself a [DisposableHandle] */ +class DisposableHandles : DisposableHandle { + private val handles = mutableListOf<DisposableHandle>() + + /** Add the provided handles to this collection. */ + fun add(vararg handles: DisposableHandle) { + this.handles.addAll(handles) + } + + /** Same as [add] */ + operator fun plusAssign(handle: DisposableHandle) { + this.handles.add(handle) + } + + /** Same as [add] */ + operator fun plusAssign(handles: Iterable<DisposableHandle>) { + this.handles.addAll(handles) + } + + /** [dispose] the current contents, then [add] the provided [handles] */ + fun replaceAll(vararg handles: DisposableHandle) { + dispose() + add(*handles) + } + + /** Dispose of all added handles and empty this collection. */ + override fun dispose() { + handles.forEach { it.dispose() } + handles.clear() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt new file mode 100644 index 000000000000..7a2f9b24700f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.statusbar.phone.ManagedProfileController +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +val ManagedProfileController.hasActiveWorkProfile: Flow<Boolean> + get() = conflatedCallbackFlow { + val callback = + object : ManagedProfileController.Callback { + override fun onManagedProfileChanged() { + trySend(hasActiveProfile()) + } + override fun onManagedProfileRemoved() { + // no-op, because the other callback will also be called. + } + } + addCallback(callback) // calls onManagedProfileChanged + awaitClose { removeCallback(callback) } + } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt index d134e60ef72f..155102c9b9a7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt @@ -21,7 +21,6 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.MediaControllerRepository import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl -import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -52,13 +51,6 @@ interface MediaDevicesModule { @Provides @SysUISingleton - fun provideLocalMediaInteractor( - repository: LocalMediaRepository, - @Application scope: CoroutineScope, - ): LocalMediaInteractor = LocalMediaInteractor(repository, scope) - - @Provides - @SysUISingleton fun provideMediaDeviceSessionRepository( intentsReceiver: AudioManagerEventsReceiver, mediaSessionManager: MediaSessionManager, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt index 8ff2837c44ef..0207d6e8e8c2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt @@ -36,10 +36,10 @@ constructor( } fun onSettingsClicked() { - volumePanelViewModel.dismissPanel() activityStarter.startActivity( - Intent(Settings.ACTION_SOUND_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + Intent(Settings.ACTION_SOUND_SETTINGS) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), true, - ) + ) { volumePanelViewModel.dismissPanel() } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt index 11b4690e59ee..e052f243f7ea 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt @@ -15,15 +15,12 @@ */ package com.android.systemui.volume.panel.component.mediaoutput.data.repository -import android.media.MediaRouter2Manager import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.media.controls.util.LocalMediaManagerFactory import javax.inject.Inject -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope interface LocalMediaRepositoryFactory { @@ -35,18 +32,14 @@ class LocalMediaRepositoryFactoryImpl @Inject constructor( private val eventsReceiver: AudioManagerEventsReceiver, - private val mediaRouter2Manager: MediaRouter2Manager, private val localMediaManagerFactory: LocalMediaManagerFactory, @Application private val coroutineScope: CoroutineScope, - @Background private val backgroundCoroutineContext: CoroutineContext, ) : LocalMediaRepositoryFactory { override fun create(packageName: String?): LocalMediaRepository = LocalMediaRepositoryImpl( eventsReceiver, localMediaManagerFactory.create(packageName), - mediaRouter2Manager, coroutineScope, - backgroundCoroutineContext, ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt new file mode 100644 index 000000000000..b0c8a4a2d478 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.volume.panel.component.mediaoutput.domain.interactor + +import android.media.session.MediaController +import android.media.session.PlaybackState +import android.os.Handler +import com.android.settingslib.volume.data.repository.MediaControllerChange +import com.android.settingslib.volume.data.repository.MediaControllerRepository +import com.android.settingslib.volume.data.repository.stateChanges +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** Allows to observe and change [MediaDeviceSession] state. */ +@OptIn(ExperimentalCoroutinesApi::class) +@VolumePanelScope +class MediaDeviceSessionInteractor +@Inject +constructor( + @Background private val backgroundCoroutineContext: CoroutineContext, + @Background private val backgroundHandler: Handler, + private val mediaControllerRepository: MediaControllerRepository, +) { + + /** [PlaybackState] changes for the [MediaDeviceSession]. */ + fun playbackState(session: MediaDeviceSession): Flow<PlaybackState?> { + return stateChanges(session) { + emit(MediaControllerChange.PlaybackStateChanged(it.playbackState)) + } + .filterIsInstance(MediaControllerChange.PlaybackStateChanged::class) + .map { it.state } + } + + /** [MediaController.PlaybackInfo] changes for the [MediaDeviceSession]. */ + fun playbackInfo(session: MediaDeviceSession): Flow<MediaController.PlaybackInfo?> { + return stateChanges(session) { + emit(MediaControllerChange.AudioInfoChanged(it.playbackInfo)) + } + .filterIsInstance(MediaControllerChange.AudioInfoChanged::class) + .map { it.info } + } + + private fun stateChanges( + session: MediaDeviceSession, + onStart: suspend FlowCollector<MediaControllerChange>.(controller: MediaController) -> Unit, + ): Flow<MediaControllerChange?> = + mediaControllerRepository.activeSessions + .flatMapLatest { controllers -> + val controller: MediaController = + findControllerForSession(controllers, session) + ?: return@flatMapLatest flowOf(null) + controller.stateChanges(backgroundHandler).onStart { onStart(controller) } + } + .flowOn(backgroundCoroutineContext) + + /** Set [MediaDeviceSession] volume to [volume]. */ + suspend fun setSessionVolume(mediaDeviceSession: MediaDeviceSession, volume: Int): Boolean { + if (!mediaDeviceSession.canAdjustVolume) { + return false + } + return withContext(backgroundCoroutineContext) { + val controller = + findControllerForSession( + mediaControllerRepository.activeSessions.value, + mediaDeviceSession, + ) + if (controller == null) { + false + } else { + controller.setVolumeTo(volume, 0) + true + } + } + } + + private fun findControllerForSession( + controllers: Collection<MediaController>, + mediaDeviceSession: MediaDeviceSession, + ): MediaController? = + controllers.firstOrNull { it.sessionToken == mediaDeviceSession.sessionToken } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt index cb16abe7e575..ea4c082f4660 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt @@ -33,23 +33,15 @@ constructor( private val mediaOutputDialogManager: MediaOutputDialogManager, ) { - fun onBarClick(session: MediaDeviceSession, expandable: Expandable) { - when (session) { - is MediaDeviceSession.Active -> { - mediaOutputDialogManager.createAndShowWithController( - session.packageName, - false, - expandable.dialogController() - ) - } - is MediaDeviceSession.Inactive -> { - mediaOutputDialogManager.createAndShowForSystemRouting( - expandable.dialogController() - ) - } - else -> { - /* do nothing */ - } + fun onBarClick(session: MediaDeviceSession, isPlaybackActive: Boolean, expandable: Expandable) { + if (isPlaybackActive) { + mediaOutputDialogManager.createAndShowWithController( + session.packageName, + false, + expandable.dialogController() + ) + } else { + mediaOutputDialogManager.createAndShowForSystemRouting(expandable.dialogController()) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt index 0f5343701ac6..e60139ecf9cc 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt @@ -17,17 +17,16 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor import android.content.pm.PackageManager +import android.media.VolumeProvider import android.media.session.MediaController -import android.os.Handler import android.util.Log import com.android.settingslib.media.MediaDevice import com.android.settingslib.volume.data.repository.LocalMediaRepository -import com.android.settingslib.volume.data.repository.MediaControllerChange import com.android.settingslib.volume.data.repository.MediaControllerRepository -import com.android.settingslib.volume.data.repository.stateChanges import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -38,12 +37,9 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -58,35 +54,40 @@ constructor( private val packageManager: PackageManager, @VolumePanelScope private val coroutineScope: CoroutineScope, @Background private val backgroundCoroutineContext: CoroutineContext, - @Background private val backgroundHandler: Handler, - mediaControllerRepository: MediaControllerRepository + mediaControllerRepository: MediaControllerRepository, ) { - /** Current [MediaDeviceSession]. Emits when the session playback changes. */ - val mediaDeviceSession: StateFlow<MediaDeviceSession> = - mediaControllerRepository.activeLocalMediaController - .flatMapLatest { it?.mediaDeviceSession() ?: flowOf(MediaDeviceSession.Inactive) } - .flowOn(backgroundCoroutineContext) - .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSession.Inactive) + private val activeMediaControllers: Flow<MediaControllers> = + mediaControllerRepository.activeSessions + .map { getMediaControllers(it) } + .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) + + /** [MediaDeviceSessions] that contains currently active sessions. */ + val activeMediaDeviceSessions: Flow<MediaDeviceSessions> = + activeMediaControllers.map { + MediaDeviceSessions( + local = it.local?.mediaDeviceSession(), + remote = it.remote?.mediaDeviceSession() + ) + } - private fun MediaController.mediaDeviceSession(): Flow<MediaDeviceSession> { - return stateChanges(backgroundHandler) - .onStart { emit(MediaControllerChange.PlaybackStateChanged(playbackState)) } - .filterIsInstance<MediaControllerChange.PlaybackStateChanged>() + /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */ + val defaultActiveMediaSession: StateFlow<MediaDeviceSession?> = + activeMediaControllers .map { - MediaDeviceSession.Active( - appLabel = getApplicationLabel(packageName) - ?: return@map MediaDeviceSession.Inactive, - packageName = packageName, - sessionToken = sessionToken, - playbackState = playbackState, - ) + when { + it.local?.playbackState?.isActive == true -> it.local.mediaDeviceSession() + it.remote?.playbackState?.isActive == true -> it.remote.mediaDeviceSession() + it.local != null -> it.local.mediaDeviceSession() + else -> null + } } - } + .flowOn(backgroundCoroutineContext) + .stateIn(coroutineScope, SharingStarted.Eagerly, null) private val localMediaRepository: SharedFlow<LocalMediaRepository> = - mediaDeviceSession - .map { (it as? MediaDeviceSession.Active)?.packageName } + defaultActiveMediaSession + .map { it?.packageName } .distinctUntilChanged() .map { localMediaRepositoryFactory.create(it) } .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) @@ -111,6 +112,54 @@ constructor( } } + /** Finds local and remote media controllers. */ + private fun getMediaControllers( + controllers: Collection<MediaController>, + ): MediaControllers { + var localController: MediaController? = null + var remoteController: MediaController? = null + val remoteMediaSessions: MutableSet<String> = mutableSetOf() + for (controller in controllers) { + val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue + when (playbackInfo.playbackType) { + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { + // MediaController can't be local if there is a remote one for the same package + if (localController?.packageName.equals(controller.packageName)) { + localController = null + } + if (!remoteMediaSessions.contains(controller.packageName)) { + remoteMediaSessions.add(controller.packageName) + if (remoteController == null) { + remoteController = controller + } + } + } + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { + if (controller.packageName in remoteMediaSessions) continue + if (localController != null) continue + localController = controller + } + } + } + return MediaControllers(local = localController, remote = remoteController) + } + + private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? { + return MediaDeviceSession( + packageName = packageName, + sessionToken = sessionToken, + canAdjustVolume = + playbackInfo != null && + playbackInfo?.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED, + appLabel = getApplicationLabel(packageName) ?: return null + ) + } + + private data class MediaControllers( + val local: MediaController?, + val remote: MediaController?, + ) + private companion object { const val TAG = "MediaOutputInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt index 1bceee9b2d34..2a2ce796a2b7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt @@ -17,26 +17,15 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.model import android.media.session.MediaSession -import android.media.session.PlaybackState /** Represents media playing on the connected device. */ -sealed interface MediaDeviceSession { +data class MediaDeviceSession( + val appLabel: CharSequence, + val packageName: String, + val sessionToken: MediaSession.Token, + val canAdjustVolume: Boolean, +) - /** Media is playing. */ - data class Active( - val appLabel: CharSequence, - val packageName: String, - val sessionToken: MediaSession.Token, - val playbackState: PlaybackState?, - ) : MediaDeviceSession - - /** Media is not playing. */ - data object Inactive : MediaDeviceSession - - /** Current media state is unknown yet. */ - data object Unknown : MediaDeviceSession -} - -/** Returns true when the audio is playing for the [MediaDeviceSession]. */ -fun MediaDeviceSession.isPlaying(): Boolean = - this is MediaDeviceSession.Active && playbackState?.isActive == true +/** Returns true when [other] controls the same sessions as [this]. */ +fun MediaDeviceSession.isTheSameSession(other: MediaDeviceSession?): Boolean = + sessionToken == other?.sessionToken diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt new file mode 100644 index 000000000000..ddc078421b9a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.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.volume.panel.component.mediaoutput.domain.model + +/** Models a pair of local and remote [MediaDeviceSession]s. */ +data class MediaDeviceSessions( + val local: MediaDeviceSession?, + val remote: MediaDeviceSession?, +) { + + companion object { + /** Returns [MediaDeviceSessions.local]. */ + val Local: (MediaDeviceSessions) -> MediaDeviceSession? = { it.local } + /** Returns [MediaDeviceSessions.remote]. */ + val Remote: (MediaDeviceSessions) -> MediaDeviceSession? = { it.remote } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt index d49cb1ea6958..2530a3a46384 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt @@ -17,24 +17,30 @@ package com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel import android.content.Context +import android.media.session.PlaybackState import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Color import com.android.systemui.common.shared.model.Icon import com.android.systemui.res.R +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession -import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** Models the UI of the Media Output Volume Panel component. */ +@OptIn(ExperimentalCoroutinesApi::class) @VolumePanelScope class MediaOutputViewModel @Inject @@ -43,25 +49,36 @@ constructor( @VolumePanelScope private val coroutineScope: CoroutineScope, private val volumePanelViewModel: VolumePanelViewModel, private val actionsInteractor: MediaOutputActionsInteractor, + private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, interactor: MediaOutputInteractor, ) { - private val mediaDeviceSession: StateFlow<MediaDeviceSession> = - interactor.mediaDeviceSession.stateIn( - coroutineScope, - SharingStarted.Eagerly, - MediaDeviceSession.Unknown, - ) + private val sessionWithPlayback: StateFlow<SessionWithPlayback?> = + interactor.defaultActiveMediaSession + .flatMapLatest { session -> + if (session == null) { + flowOf(null) + } else { + mediaDeviceSessionInteractor.playbackState(session).map { playback -> + playback?.let { SessionWithPlayback(session, it) } + } + } + } + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + null, + ) val connectedDeviceViewModel: StateFlow<ConnectedDeviceViewModel?> = - combine(mediaDeviceSession, interactor.currentConnectedDevice) { + combine(sessionWithPlayback, interactor.currentConnectedDevice) { mediaDeviceSession, currentConnectedDevice -> ConnectedDeviceViewModel( - if (mediaDeviceSession.isPlaying()) { + if (mediaDeviceSession?.playback?.isActive == true) { context.getString( R.string.media_output_label_title, - (mediaDeviceSession as MediaDeviceSession.Active).appLabel + mediaDeviceSession.session.appLabel ) } else { context.getString(R.string.media_output_title_without_playing) @@ -76,10 +93,10 @@ constructor( ) val deviceIconViewModel: StateFlow<DeviceIconViewModel?> = - combine(mediaDeviceSession, interactor.currentConnectedDevice) { + combine(sessionWithPlayback, interactor.currentConnectedDevice) { mediaDeviceSession, currentConnectedDevice -> - if (mediaDeviceSession.isPlaying()) { + if (mediaDeviceSession?.playback?.isActive == true) { val icon = currentConnectedDevice?.icon?.let { Icon.Loaded(it, null) } ?: Icon.Resource( @@ -112,7 +129,14 @@ constructor( ) fun onBarClick(expandable: Expandable) { - actionsInteractor.onBarClick(mediaDeviceSession.value, expandable) + sessionWithPlayback.value?.let { + actionsInteractor.onBarClick(it.session, it.playback.isActive, expandable) + } volumePanelViewModel.dismissPanel() } + + private data class SessionWithPlayback( + val session: MediaDeviceSession, + val playback: PlaybackState, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt index 30715d167c25..f260d6130c4b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt @@ -56,7 +56,7 @@ constructor( val isAvailable: StateFlow<Boolean> = availabilityCriteria.isAvailable().stateIn(scope, SharingStarted.Eagerly, true) - val spatialAudioButtonByEnabled: StateFlow<List<SpatialAudioButtonViewModel>> = + val spatialAudioButtons: StateFlow<List<SpatialAudioButtonViewModel>> = combine(interactor.isEnabled, interactor.isAvailable) { currentIsEnabled, isAvailable -> SpatialAudioEnabledModel.values .filter { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt deleted file mode 100644 index 6b62074e023d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.volume.panel.component.volume.domain.interactor - -import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor -import com.android.settingslib.volume.domain.model.RoutingSession -import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -/** Provides a remote media casting state. */ -@VolumePanelScope -class CastVolumeInteractor -@Inject -constructor( - @VolumePanelScope private val coroutineScope: CoroutineScope, - private val localMediaInteractor: LocalMediaInteractor, -) { - - /** Returns a list of [RoutingSession] to show in the UI. */ - val remoteRoutingSessions: StateFlow<List<RoutingSession>> = - localMediaInteractor.remoteRoutingSessions - .map { it.filter { routingSession -> routingSession.isVolumeSeekBarEnabled } } - .stateIn(coroutineScope, SharingStarted.Eagerly, emptyList()) - - /** Sets [routingSession] volume to [volume]. */ - suspend fun setVolume(routingSession: RoutingSession, volume: Int) { - localMediaInteractor.adjustSessionVolume(routingSession.routingSessionInfo.id, volume) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index 1b732081a12a..3242c2814bc5 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -80,7 +80,7 @@ constructor( ) { model, isEnabled, ringerMode -> model.toState(isEnabled, ringerMode) } - .stateIn(coroutineScope, SharingStarted.Eagerly, EmptyState) + .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) override fun onValueChanged(state: SliderState, newValue: Float) { val audioViewModel = state as? State @@ -116,6 +116,7 @@ constructor( isEnabled = isEnabled, a11yStep = volumeRange.step, audioStreamModel = this, + isMutable = audioVolumeInteractor.isMutable(audioStream), ) } @@ -160,20 +161,10 @@ constructor( override val disabledMessage: String?, override val isEnabled: Boolean, override val a11yStep: Int, + override val isMutable: Boolean, val audioStreamModel: AudioStreamModel, ) : SliderState - private data object EmptyState : SliderState { - override val value: Float = 0f - override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f - override val icon: Icon? = null - override val valueText: String = "" - override val label: String = "" - override val disabledMessage: String? = null - override val a11yStep: Int = 0 - override val isEnabled: Boolean = true - } - @AssistedFactory interface Factory { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt index 86b2d73de3e3..73c8bbfce6d9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt @@ -17,11 +17,11 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context -import com.android.settingslib.volume.domain.model.RoutingSession +import android.media.session.MediaController.PlaybackInfo import com.android.systemui.common.shared.model.Icon import com.android.systemui.res.R -import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor -import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -30,30 +30,29 @@ import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch class CastVolumeSliderViewModel @AssistedInject constructor( - @Assisted private val routingSession: RoutingSession, + @Assisted private val session: MediaDeviceSession, @Assisted private val coroutineScope: CoroutineScope, private val context: Context, - mediaOutputInteractor: MediaOutputInteractor, + private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, private val volumeSliderInteractor: VolumeSliderInteractor, - private val castVolumeInteractor: CastVolumeInteractor, ) : SliderViewModel { - private val volumeRange = 0..routingSession.routingSessionInfo.volumeMax - override val slider: StateFlow<SliderState> = - combine(mediaOutputInteractor.currentConnectedDevice) { _ -> getCurrentState() } - .stateIn(coroutineScope, SharingStarted.Eagerly, getCurrentState()) + mediaDeviceSessionInteractor + .playbackInfo(session) + .mapNotNull { it?.getCurrentState() } + .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) override fun onValueChanged(state: SliderState, newValue: Float) { coroutineScope.launch { - castVolumeInteractor.setVolume(routingSession, newValue.roundToInt()) + mediaDeviceSessionInteractor.setSessionVolume(session, newValue.roundToInt()) } } @@ -61,15 +60,16 @@ constructor( // do nothing because this action isn't supported for Cast sliders. } - private fun getCurrentState(): State = - State( - value = routingSession.routingSessionInfo.volume.toFloat(), + private fun PlaybackInfo.getCurrentState(): State { + val volumeRange = 0..maxVolume + return State( + value = currentVolume.toFloat(), valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(), icon = Icon.Resource(R.drawable.ic_cast, null), valueText = SliderViewModel.formatValue( volumeSliderInteractor.processVolumeToValue( - volume = routingSession.routingSessionInfo.volume, + volume = currentVolume, volumeRange = volumeRange, ) ), @@ -77,6 +77,7 @@ constructor( isEnabled = true, a11yStep = 1 ) + } private data class State( override val value: Float, @@ -89,13 +90,15 @@ constructor( ) : SliderState { override val disabledMessage: String? get() = null + override val isMutable: Boolean + get() = false } @AssistedFactory interface Factory { fun create( - routingSession: RoutingSession, + session: MediaDeviceSession, coroutineScope: CoroutineScope, ): CastVolumeSliderViewModel } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt index b87d0a786740..8eb0b8947c37 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt @@ -36,4 +36,17 @@ sealed interface SliderState { */ val a11yStep: Int val disabledMessage: String? + val isMutable: Boolean + + data object Empty : SliderState { + override val value: Float = 0f + override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f + override val icon: Icon? = null + override val valueText: String = "" + override val label: String = "" + override val disabledMessage: String? = null + override val a11yStep: Int = 0 + override val isEnabled: Boolean = true + override val isMutable: Boolean = false + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt index aaee24b9357f..4e9a45635f7b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt @@ -18,9 +18,10 @@ package com.android.systemui.volume.panel.component.volume.ui.viewmodel import android.media.AudioManager import com.android.settingslib.volume.shared.model.AudioStream +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor -import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying -import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isTheSameSession import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel @@ -29,17 +30,15 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch /** @@ -52,50 +51,34 @@ class AudioVolumeComponentViewModel @Inject constructor( @VolumePanelScope private val scope: CoroutineScope, - castVolumeInteractor: CastVolumeInteractor, mediaOutputInteractor: MediaOutputInteractor, + private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory, private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory, ) { - private val remoteSessionsViewModels: Flow<List<SliderViewModel>> = - castVolumeInteractor.remoteRoutingSessions.transformLatest { routingSessions -> - coroutineScope { - emit( - routingSessions.map { routingSession -> - castVolumeSliderViewModelFactory.create(routingSession, this) - } - ) - } - } - private val streamViewModels: Flow<List<SliderViewModel>> = - flowOf( - listOf( - AudioStream(AudioManager.STREAM_MUSIC), - AudioStream(AudioManager.STREAM_VOICE_CALL), - AudioStream(AudioManager.STREAM_RING), - AudioStream(AudioManager.STREAM_NOTIFICATION), - AudioStream(AudioManager.STREAM_ALARM), - ) - ) - .transformLatest { streams -> + val sliderViewModels: StateFlow<List<SliderViewModel>> = + combineTransform( + mediaOutputInteractor.activeMediaDeviceSessions, + mediaOutputInteractor.defaultActiveMediaSession, + ) { activeSessions, defaultSession -> coroutineScope { - emit( - streams.map { stream -> - streamSliderViewModelFactory.create( - AudioStreamSliderViewModel.FactoryAudioStreamWrapper(stream), - this, - ) + val viewModels = buildList { + if (defaultSession?.isTheSameSession(activeSessions.remote) == true) { + addRemoteViewModelIfNeeded(this, activeSessions.remote) + addStreamViewModel(this, AudioManager.STREAM_MUSIC) + } else { + addStreamViewModel(this, AudioManager.STREAM_MUSIC) + addRemoteViewModelIfNeeded(this, activeSessions.remote) } - ) - } - } - val sliderViewModels: StateFlow<List<SliderViewModel>> = - combine(remoteSessionsViewModels, streamViewModels) { - remoteSessionsViewModels, - streamViewModels -> - remoteSessionsViewModels + streamViewModels + addStreamViewModel(this, AudioManager.STREAM_VOICE_CALL) + addStreamViewModel(this, AudioManager.STREAM_RING) + addStreamViewModel(this, AudioManager.STREAM_NOTIFICATION) + addStreamViewModel(this, AudioManager.STREAM_ALARM) + } + emit(viewModels) + } } .stateIn(scope, SharingStarted.Eagerly, emptyList()) @@ -103,12 +86,41 @@ constructor( val isExpanded: StateFlow<Boolean> = merge( - mutableIsExpanded.onStart { emit(false) }, - mediaOutputInteractor.mediaDeviceSession.map { !it.isPlaying() }, + mutableIsExpanded, + mediaOutputInteractor.defaultActiveMediaSession.flatMapLatest { + if (it == null) flowOf(true) + else mediaDeviceSessionInteractor.playbackState(it).map { it?.isActive != true } + }, ) .stateIn(scope, SharingStarted.Eagerly, false) fun onExpandedChanged(isExpanded: Boolean) { scope.launch { mutableIsExpanded.emit(isExpanded) } } + + private fun CoroutineScope.addRemoteViewModelIfNeeded( + list: MutableList<SliderViewModel>, + remoteMediaDeviceSession: MediaDeviceSession? + ) { + if (remoteMediaDeviceSession?.canAdjustVolume == true) { + val viewModel = + castVolumeSliderViewModelFactory.create( + remoteMediaDeviceSession, + this, + ) + list.add(viewModel) + } + } + + private fun CoroutineScope.addStreamViewModel( + list: MutableList<SliderViewModel>, + stream: Int, + ) { + val viewModel = + streamSliderViewModelFactory.create( + AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)), + this, + ) + list.add(viewModel) + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt index d430e65770fd..c728fefa77e6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt @@ -42,7 +42,6 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) - volumePanelFlag.assertNewVolumePanel() setContent { VolumePanelRoot(viewModel = viewModel, onDismiss = ::finish) } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 7931fab91f46..e48b6397457c 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -363,8 +363,8 @@ public final class WMShell implements }, mSysUiMainExecutor); mCommandQueue.addCallback(new CommandQueue.Callbacks() { @Override - public void enterDesktop(int displayId) { - desktopMode.enterDesktop(displayId); + public void moveFocusedTaskToDesktop(int displayId) { + desktopMode.moveFocusedTaskToDesktop(displayId); } @Override public void moveFocusedTaskToFullscreen(int displayId) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt index b73e4e6ab015..9182e4101f36 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt @@ -36,6 +36,7 @@ import org.junit.runner.RunWith import org.mockito.Mockito.any import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule @SmallTest @RunWith(AndroidTestingRunner::class) @@ -44,8 +45,8 @@ class DialogTransitionAnimatorTest : SysuiTestCase() { private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator private val attachedViews = mutableSetOf<View>() - val interactionJankMonitor = Kosmos().interactionJankMonitor - @get:Rule val rule = MockitoJUnit.rule() + private val interactionJankMonitor = Kosmos().interactionJankMonitor + @get:Rule val rule: MockitoRule = MockitoJUnit.rule() @Before fun setUp() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 072569d0e69b..33a6010d816c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -20,6 +20,8 @@ import android.content.pm.PackageManager import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricManager +import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView import android.hardware.face.FaceSensorPropertiesInternal @@ -35,6 +37,7 @@ import android.view.View import android.view.WindowInsets import android.view.WindowManager import android.widget.ScrollView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.jank.InteractionJankMonitor @@ -82,6 +85,7 @@ import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever + private const val OP_PACKAGE_NAME = "biometric.testapp" @RunWith(AndroidJUnit4::class) @@ -386,10 +390,38 @@ open class AuthContainerViewTest : SysuiTestCase() { } @Test + fun testShowBiometricUI_ContentViewWithMoreOptionsButton() { + mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + var isButtonClicked = false + val contentView = + PromptContentViewWithMoreOptionsButton.Builder() + .setMoreOptionsButtonListener( + fakeExecutor) { _, _ -> isButtonClicked = true } + .build() + + val container = + initializeFingerprintContainer(contentViewWithMoreOptionsButton = contentView) + + waitForIdleSync() + + assertThat(container.hasCredentialView()).isFalse() + assertThat(container.hasConstraintBiometricPrompt()).isTrue() + + // TODO(b/328843028): Use button.performClick() instead of calling + // onContentViewMoreOptionsButtonPressed() directly, and check |isButtonClicked| is true. + container.mBiometricCallback.onContentViewMoreOptionsButtonPressed() + waitForIdleSync() + // container is gone + assertThat(container.mContainerState).isEqualTo(5) + } + + @Test fun testShowCredentialUI_withDescription() { - val container = initializeFingerprintContainer( - authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL - ) + val container = + initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL + ) waitForIdleSync() assertThat(container.hasCredentialView()).isTrue() @@ -397,14 +429,44 @@ open class AuthContainerViewTest : SysuiTestCase() { } @Test - @Ignore("b/302735104") - fun testShowCredentialUI_withCustomBp() { - mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP) - val container = initializeFingerprintContainer( - authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL, - isUsingContentView = true - ) - checkBpShowsForCredentialAndGoToCredential(container) + fun testShowCredentialUI_withVerticalListContentView() { + mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val container = + initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL, + verticalListContentView = PromptVerticalListContentView.Builder().build() + ) + // Two-step credential view should show - + // 1. biometric prompt without sensor 2. credential view ui + waitForIdleSync() + assertThat(container.hasConstraintBiometricPrompt()).isTrue() + assertThat(container.hasCredentialView()).isFalse() + + container.animateToCredentialUI(false) + waitForIdleSync() + // TODO(b/302735104): Check the reason why hasConstraintBiometricPrompt() is still true + // assertThat(container.hasConstraintBiometricPrompt()).isFalse() + assertThat(container.hasCredentialView()).isTrue() + } + + @Test + fun testShowCredentialUI_withContentViewWithMoreOptionsButton() { + mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val contentView = + PromptContentViewWithMoreOptionsButton.Builder() + .setMoreOptionsButtonListener(fakeExecutor) { _, _ -> } + .build() + val container = + initializeFingerprintContainer( + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL, + contentViewWithMoreOptionsButton = contentView + ) + waitForIdleSync() + + assertThat(container.hasCredentialView()).isTrue() + assertThat(container.hasBiometricPrompt()).isFalse() } @Test @@ -509,12 +571,13 @@ open class AuthContainerViewTest : SysuiTestCase() { private fun initializeFingerprintContainer( authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, addToView: Boolean = true, - isUsingContentView: Boolean = false, + verticalListContentView: PromptVerticalListContentView? = null, + contentViewWithMoreOptionsButton: PromptContentViewWithMoreOptionsButton? = null, ) = initializeContainer( TestAuthContainerView( authenticators = authenticators, fingerprintProps = fingerprintSensorPropertiesInternal(), - isUsingContentView = isUsingContentView, + verticalListContentView = verticalListContentView, ), addToView ) @@ -548,7 +611,8 @@ open class AuthContainerViewTest : SysuiTestCase() { authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK, fingerprintProps: List<FingerprintSensorPropertiesInternal> = listOf(), faceProps: List<FaceSensorPropertiesInternal> = listOf(), - isUsingContentView: Boolean = false, + verticalListContentView: PromptVerticalListContentView? = null, + contentViewWithMoreOptionsButton: PromptContentViewWithMoreOptionsButton? = null, ) : AuthContainerView( Config().apply { mContext = this@AuthContainerViewTest.context @@ -558,8 +622,10 @@ open class AuthContainerViewTest : SysuiTestCase() { mSkipAnimation = true mPromptInfo = PromptInfo().apply { this.authenticators = authenticators - if (isUsingContentView) { - this.contentView = PromptVerticalListContentView.Builder().build() + if (verticalListContentView != null) { + this.contentView = verticalListContentView + } else if (contentViewWithMoreOptionsButton != null) { + this.contentView = contentViewWithMoreOptionsButton } } mOpPackageName = OP_PACKAGE_NAME @@ -616,19 +682,11 @@ open class AuthContainerViewTest : SysuiTestCase() { val layoutParams = AuthContainerView.getLayoutParams(windowToken, "") assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.systemBars()) == 0).isTrue() } - - private fun checkBpShowsForCredentialAndGoToCredential(container: TestAuthContainerView) { - waitForIdleSync() - assertThat(container.hasBiometricPrompt()).isTrue() - assertThat(container.hasCredentialView()).isFalse() - - container.animateToCredentialUI(false) - waitForIdleSync() - assertThat(container.hasBiometricPrompt()).isFalse() - assertThat(container.hasCredentialView()).isTrue() - } } +private fun AuthContainerView.hasConstraintBiometricPrompt() = + (findViewById<ConstraintLayout>(R.id.biometric_prompt_constraint_layout)?.childCount ?: 0) > 0 + private fun AuthContainerView.hasBiometricPrompt() = (findViewById<ScrollView>(R.id.biometric_scrollview)?.childCount ?: 0) > 0 diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt index 81d4e8302c3f..df0e5a718ed9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.biometrics.data.repository import android.hardware.biometrics.BiometricManager import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView import androidx.test.filters.SmallTest @@ -26,8 +27,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList @@ -58,6 +61,7 @@ class PromptRepositoryImplTest : SysuiTestCase() { private val testScope = TestScope() private val faceSettings = FakeFaceSettingsRepository() + private val fakeExecutor = FakeExecutor(FakeSystemClock()) @Mock private lateinit var authController: AuthController @@ -135,7 +139,7 @@ class PromptRepositoryImplTest : SysuiTestCase() { } @Test - fun showBpWithoutIconForCredential_withCustomBp() = + fun showBpWithoutIconForCredential_withVerticalListContentView() = testScope.runTest { mSetFlagsRule.enableFlags(Flags.FLAG_CONSTRAINT_BP) mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) @@ -161,8 +165,37 @@ class PromptRepositoryImplTest : SysuiTestCase() { } @Test + fun showBpWithoutIconForCredential_withContentViewWithMoreOptionsButton() = + testScope.runTest { + mSetFlagsRule.enableFlags(Flags.FLAG_CONSTRAINT_BP) + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val promptInfo = + PromptInfo().apply { + authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL + contentView = + PromptContentViewWithMoreOptionsButton.Builder() + .setMoreOptionsButtonListener(fakeExecutor) { _, _ -> } + .build() + } + for (case in + listOf( + PromptKind.Biometric(), + PromptKind.Pin, + PromptKind.Password, + PromptKind.Pattern + )) { + repository.setPrompt(promptInfo, USER_ID, CHALLENGE, case, OP_PACKAGE_NAME) + repository.setShouldShowBpWithoutIconForCredential(promptInfo) + + assertThat(repository.showBpWithoutIconForCredential.value).isFalse() + } + } + + @Test fun showBpWithoutIconForCredential_withDescription() = testScope.runTest { + mSetFlagsRule.enableFlags(Flags.FLAG_CONSTRAINT_BP) + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) for (case in listOf( PromptKind.Biometric(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt index 8a46c0c6da9f..2172bc5ee8e1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt @@ -1,6 +1,8 @@ package com.android.systemui.biometrics.domain.interactor +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo +import android.hardware.biometrics.PromptVerticalListContentView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.Utils @@ -10,6 +12,8 @@ import com.android.systemui.biometrics.domain.model.BiometricPromptRequest import com.android.systemui.biometrics.promptInfo import com.android.systemui.biometrics.shared.model.BiometricUserInfo import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -43,6 +47,7 @@ class PromptCredentialInteractorTest : SysuiTestCase() { private val testScope = TestScope(testDispatcher) private val biometricPromptRepository = FakePromptRepository() private val credentialInteractor = FakeCredentialInteractor() + private val fakeExecutor = FakeExecutor(FakeSystemClock()) private lateinit var interactor: PromptCredentialInteractor @@ -90,6 +95,82 @@ class PromptCredentialInteractorTest : SysuiTestCase() { assertThat(prompt).isNull() } + @Test + fun testShowTitleOnlyValue_description() = + testScope.runTest { + val title = "what a prompt" + val subtitle = "s" + val description = "something to see" + + val showTitleOnly by collectLastValue(interactor.showTitleOnly) + + interactor.useCredentialsForAuthentication( + PromptInfo().also { + it.title = title + it.description = description + it.subtitle = subtitle + }, + kind = Utils.CREDENTIAL_PIN, + userId = USER_ID, + challenge = OPERATION_ID, + opPackageName = OP_PACKAGE_NAME + ) + assertThat(showTitleOnly).isFalse() + } + + @Test + fun testShowTitleOnlyValue_verticalListContentView() = + testScope.runTest { + val title = "what a prompt" + val subtitle = "s" + val description = "something to see" + val contentView = PromptVerticalListContentView.Builder().build() + + val showTitleOnly by collectLastValue(interactor.showTitleOnly) + + interactor.useCredentialsForAuthentication( + PromptInfo().also { + it.title = title + it.description = description + it.subtitle = subtitle + it.contentView = contentView + }, + kind = Utils.CREDENTIAL_PIN, + userId = USER_ID, + challenge = OPERATION_ID, + opPackageName = OP_PACKAGE_NAME + ) + assertThat(showTitleOnly).isTrue() + } + + @Test + fun testShowTitleOnlyValue_ContentViewWithButton() = + testScope.runTest { + val title = "what a prompt" + val subtitle = "s" + val description = "something to see" + val contentView = + PromptContentViewWithMoreOptionsButton.Builder() + .setMoreOptionsButtonListener(fakeExecutor) { _, _ -> } + .build() + + val showTitleOnly by collectLastValue(interactor.showTitleOnly) + + interactor.useCredentialsForAuthentication( + PromptInfo().also { + it.title = title + it.description = description + it.subtitle = subtitle + it.contentView = contentView + }, + kind = Utils.CREDENTIAL_PIN, + userId = USER_ID, + challenge = OPERATION_ID, + opPackageName = OP_PACKAGE_NAME + ) + assertThat(showTitleOnly).isFalse() + } + @Test fun usePinCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PIN) @Test fun usePasswordCredentialForPrompt() = useCredentialForPrompt(Utils.CREDENTIAL_PASSWORD) @@ -106,12 +187,14 @@ class PromptCredentialInteractorTest : SysuiTestCase() { val title = "what a prompt" val subtitle = "s" val description = "something to see" + val contentView = PromptVerticalListContentView.Builder().build() interactor.useCredentialsForAuthentication( PromptInfo().also { it.title = title it.description = description it.subtitle = subtitle + it.contentView = contentView }, kind = kind, userId = USER_ID, @@ -122,6 +205,7 @@ class PromptCredentialInteractorTest : SysuiTestCase() { assertThat(prompt?.title).isEqualTo(title) assertThat(prompt?.subtitle).isEqualTo(subtitle) assertThat(prompt?.description).isEqualTo(description) + assertThat(prompt?.contentView).isEqualTo(contentView) assertThat(prompt?.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) assertThat(prompt?.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) assertThat(prompt) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt index 8fab2332c00e..d10b93534f3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt @@ -2,6 +2,7 @@ package com.android.systemui.biometrics.domain.model import android.graphics.Bitmap import android.hardware.biometrics.PromptContentItemBulletedText +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptVerticalListContentView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -11,6 +12,7 @@ import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricUserInfo import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 @@ -59,7 +61,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { assertThat(request.title).isEqualTo(title) assertThat(request.subtitle).isEqualTo(subtitle) assertThat(request.description).isEqualTo(description) - assertThat(request.contentView).isEqualTo(contentView) + assertThat(request.contentView).isSameInstanceAs(contentView) assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) assertThat(request.modalities) @@ -67,6 +69,29 @@ class BiometricPromptRequestTest : SysuiTestCase() { } @Test + fun biometricRequestContentViewWithMoreOptionsButtonFromPromptInfo() { + val title = "what" + val description = "request" + val executor = MoreExecutors.directExecutor() + val contentView = + PromptContentViewWithMoreOptionsButton.Builder() + .setDescription("test") + .setMoreOptionsButtonListener(executor) { _, _ -> } + .build() + + val fpPros = fingerprintSensorPropertiesInternal().first() + val request = + BiometricPromptRequest.Biometric( + promptInfo(title = title, description = description, contentView = contentView), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID), + BiometricModalities(fingerprintProperties = fpPros), + OP_PACKAGE_NAME, + ) + assertThat(request.contentView).isSameInstanceAs(contentView) + } + + @Test fun biometricRequestLogoBitmapFromPromptInfo() { val logoBitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888) val fpPros = fingerprintSensorPropertiesInternal().first() @@ -89,6 +114,12 @@ class BiometricPromptRequestTest : SysuiTestCase() { val subtitle = "a" val description = "request" val stealth = true + val contentView = + PromptVerticalListContentView.Builder() + .setDescription("content description") + .addListItem(PromptContentItemBulletedText("content item 1")) + .addListItem(PromptContentItemBulletedText("content item 2"), 1) + .build() val toCheck = listOf( @@ -97,6 +128,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { title = title, subtitle = subtitle, description = description, + contentView = contentView, credentialTitle = null, credentialSubtitle = null, credentialDescription = null, @@ -106,6 +138,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { ), BiometricPromptRequest.Credential.Password( promptInfo( + contentView = contentView, credentialTitle = title, credentialSubtitle = subtitle, credentialDescription = description, @@ -117,6 +150,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { promptInfo( subtitle = subtitle, description = description, + contentView = contentView, credentialTitle = title, credentialSubtitle = null, credentialDescription = null, @@ -131,6 +165,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { assertThat(request.title).isEqualTo(title) assertThat(request.subtitle).isEqualTo(subtitle) assertThat(request.description).isEqualTo(description) + assertThat(request.contentView).isEqualTo(contentView) assertThat(request.userInfo).isEqualTo(BiometricUserInfo(USER_ID)) assertThat(request.operationInfo).isEqualTo(BiometricOperationInfo(OPERATION_ID)) if (request is BiometricPromptRequest.Credential.Pattern) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 7db4ca966890..5b0df5d05703 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -25,6 +25,7 @@ import android.graphics.drawable.BitmapDrawable import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptContentView +import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView import android.hardware.face.FaceSensorPropertiesInternal @@ -122,6 +123,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa private lateinit var viewModel: PromptViewModel private lateinit var iconViewModel: PromptIconViewModel private lateinit var promptContentView: PromptContentView + private lateinit var promptContentViewWithMoreOptionsButton: + PromptContentViewWithMoreOptionsButton @Before fun setup() { @@ -163,6 +166,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa .addListItem(PromptContentItemBulletedText("content item 2"), 1) .build() + promptContentViewWithMoreOptionsButton = + PromptContentViewWithMoreOptionsButton.Builder() + .setDescription("test") + .setMoreOptionsButtonListener(fakeExecutor, { _, _ -> }) + .build() + viewModel = PromptViewModel( displayStateInteractor, @@ -1254,7 +1263,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - fun descriptionOverriddenByContentView() = + fun descriptionOverriddenByVerticalListContentView() = runGenericTest(contentView = promptContentView, description = "test description") { mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) @@ -1266,6 +1275,21 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test + fun descriptionOverriddenByContentViewWithMoreOptionsButton() = + runGenericTest( + contentView = promptContentViewWithMoreOptionsButton, + description = "test description" + ) { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP) + val contentView by collectLastValue(viewModel.contentView) + val description by collectLastValue(viewModel.description) + + assertThat(description).isEqualTo("") + assertThat(contentView).isEqualTo(promptContentViewWithMoreOptionsButton) + } + + @Test fun descriptionWithoutContentView() = runGenericTest(description = "test description") { mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java index 206babf9ec44..09675e28f5da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import android.testing.AndroidTestingRunner; +import androidx.lifecycle.ViewModel; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; @@ -56,7 +57,8 @@ public class ComplicationViewModelTransformerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); when(mFactory.create(Mockito.any(), Mockito.any())).thenReturn(mComponent); when(mComponent.getViewModelProvider()).thenReturn(mViewModelProvider); - when(mViewModelProvider.get(Mockito.any(), Mockito.any())).thenReturn(mViewModel); + when(mViewModelProvider.get(Mockito.any(), Mockito.<Class<ViewModel>>any())) + .thenReturn(mViewModel); } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt index 890b9aec69bf..ae77d1f590e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/management/ControlsRequestReceiverTest.kt @@ -24,6 +24,9 @@ import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.content.pm.PackageManager +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable import android.os.UserHandle import android.service.controls.Control import android.service.controls.ControlsProviderService @@ -176,6 +179,64 @@ class ControlsRequestReceiverTest : SysuiTestCase() { assertNull(wrapper.intent) } + @Test + fun testClassNotFoundExceptionComponent_noCrash() { + val bundle = Bundle().apply { + putParcelable(Intent.EXTRA_COMPONENT_NAME, PrivateParcelable()) + putParcelable(ControlsProviderService.EXTRA_CONTROL, control) + } + val parcel = Parcel.obtain() + bundle.writeToParcel(parcel, 0) + + parcel.setDataPosition(0) + + val badIntent = Intent(ControlsProviderService.ACTION_ADD_CONTROL).apply { + parcel.readBundle()?.let { putExtras(it) } + } + receiver.onReceive(wrapper, badIntent) + + assertNull(wrapper.intent) + } + + @Test + fun testClassNotFoundExceptionControl_noCrash() { + val bundle = Bundle().apply { + putParcelable(Intent.EXTRA_COMPONENT_NAME, componentName) + putParcelable(ControlsProviderService.EXTRA_CONTROL, PrivateParcelable()) + } + val parcel = Parcel.obtain() + bundle.writeToParcel(parcel, 0) + + parcel.setDataPosition(0) + + val badIntent = Intent(ControlsProviderService.ACTION_ADD_CONTROL).apply { + parcel.readBundle()?.let { putExtras(it) } + } + receiver.onReceive(wrapper, badIntent) + + assertNull(wrapper.intent) + } + + @Test + fun testMissingComponentName_noCrash() { + val badIntent = Intent(ControlsProviderService.ACTION_ADD_CONTROL).apply { + putExtra(ControlsProviderService.EXTRA_CONTROL, control) + } + receiver.onReceive(wrapper, badIntent) + + assertNull(wrapper.intent) + } + + @Test + fun testMissingControl_noCrash() { + val badIntent = Intent(ControlsProviderService.ACTION_ADD_CONTROL).apply { + putExtra(Intent.EXTRA_COMPONENT_NAME, componentName) + } + receiver.onReceive(wrapper, badIntent) + + assertNull(wrapper.intent) + } + class MyWrapper(context: Context) : ContextWrapper(context) { var intent: Intent? = null @@ -189,4 +250,20 @@ class ControlsRequestReceiverTest : SysuiTestCase() { this.intent = intent } } + + class PrivateParcelable : Parcelable { + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) {} + + companion object CREATOR : Parcelable.Creator<PrivateParcelable?> { + override fun createFromParcel(source: Parcel?): PrivateParcelable { + return PrivateParcelable() + } + + override fun newArray(size: Int): Array<PrivateParcelable?> { + return arrayOfNulls(size) + } + } + } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt index 66fdf538e284..933ddb5739e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt @@ -16,25 +16,22 @@ package com.android.systemui.haptics.slider -import android.os.VibrationAttributes import android.os.VibrationEffect import android.view.VelocityTracker import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq +import com.android.systemui.haptics.vibratorHelper +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.time.fakeSystemClock import kotlin.math.max import kotlin.test.assertEquals +import kotlin.test.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.times -import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest @@ -42,10 +39,10 @@ import org.mockito.MockitoAnnotations class SliderHapticFeedbackProviderTest : SysuiTestCase() { @Mock private lateinit var velocityTracker: VelocityTracker - @Mock private lateinit var vibratorHelper: VibratorHelper + + private val kosmos = testKosmos() private val config = SliderHapticFeedbackConfig() - private val clock = FakeSystemClock() private val lowTickDuration = 12 // Mocked duration of a low tick private val dragTextureThresholdMillis = @@ -55,250 +52,278 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) - whenever(vibratorHelper.getPrimitiveDurations(any())) - .thenReturn(intArrayOf(lowTickDuration)) whenever(velocityTracker.isAxisSupported(config.velocityAxis)).thenReturn(true) whenever(velocityTracker.getAxisVelocity(config.velocityAxis)) .thenReturn(config.maxVelocityToScale) + + kosmos.vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] = + lowTickDuration sliderHapticFeedbackProvider = - SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, config, clock) + SliderHapticFeedbackProvider( + kosmos.vibratorHelper, + velocityTracker, + config, + kosmos.fakeSystemClock, + ) } @Test - fun playHapticAtLowerBookend_playsClick() { - val vibration = - VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_CLICK, - sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), - ) - .compose() - - sliderHapticFeedbackProvider.onLowerBookend() - - verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java)) - } + fun playHapticAtLowerBookend_playsClick() = + with(kosmos) { + val vibration = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_CLICK, + sliderHapticFeedbackProvider.scaleOnEdgeCollision( + config.maxVelocityToScale + ), + ) + .compose() + + sliderHapticFeedbackProvider.onLowerBookend() + + assertTrue(vibratorHelper.hasVibratedWithEffects(vibration)) + } @Test - fun playHapticAtLowerBookend_twoTimes_playsClickOnlyOnce() { - val vibration = - VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_CLICK, - sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale) - ) - .compose() - - sliderHapticFeedbackProvider.onLowerBookend() - sliderHapticFeedbackProvider.onLowerBookend() - - verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java)) - } + fun playHapticAtLowerBookend_twoTimes_playsClickOnlyOnce() = + with(kosmos) { + val vibration = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_CLICK, + sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale) + ) + .compose() + + sliderHapticFeedbackProvider.onLowerBookend() + sliderHapticFeedbackProvider.onLowerBookend() + + assertTrue(vibratorHelper.hasVibratedWithEffects(vibration)) + } @Test - fun playHapticAtUpperBookend_playsClick() { - val vibration = - VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_CLICK, - sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), - ) - .compose() - - sliderHapticFeedbackProvider.onUpperBookend() + fun playHapticAtUpperBookend_playsClick() = + with(kosmos) { + val vibration = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_CLICK, + sliderHapticFeedbackProvider.scaleOnEdgeCollision( + config.maxVelocityToScale + ), + ) + .compose() + + sliderHapticFeedbackProvider.onUpperBookend() + + assertTrue(vibratorHelper.hasVibratedWithEffects(vibration)) + } - verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java)) - } + @Test + fun playHapticAtUpperBookend_twoTimes_playsClickOnlyOnce() = + with(kosmos) { + val vibration = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_CLICK, + sliderHapticFeedbackProvider.scaleOnEdgeCollision( + config.maxVelocityToScale + ), + ) + .compose() + + sliderHapticFeedbackProvider.onUpperBookend() + sliderHapticFeedbackProvider.onUpperBookend() + + assertEquals(/* expected=*/ 1, vibratorHelper.timesVibratedWithEffect(vibration)) + } @Test - fun playHapticAtUpperBookend_twoTimes_playsClickOnlyOnce() { - val vibration = - VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_CLICK, - sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), + fun playHapticAtProgress_onQuickSuccession_playsLowTicksOnce() = + with(kosmos) { + // GIVEN max velocity and slider progress + val progress = 1f + val expectedScale = + sliderHapticFeedbackProvider.scaleOnDragTexture( + config.maxVelocityToScale, + progress, ) - .compose() + val ticks = VibrationEffect.startComposition() + repeat(config.numberOfLowTicks) { + ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale) + } - sliderHapticFeedbackProvider.onUpperBookend() - sliderHapticFeedbackProvider.onUpperBookend() + // GIVEN system running for 1s + fakeSystemClock.advanceTime(1000) - verify(vibratorHelper, times(1)) - .vibrate(eq(vibration), any(VibrationAttributes::class.java)) - } + // WHEN two calls to play occur immediately + sliderHapticFeedbackProvider.onProgress(progress) + sliderHapticFeedbackProvider.onProgress(progress) - @Test - fun playHapticAtProgress_onQuickSuccession_playsLowTicksOnce() { - // GIVEN max velocity and slider progress - val progress = 1f - val expectedScale = - sliderHapticFeedbackProvider.scaleOnDragTexture( - config.maxVelocityToScale, - progress, - ) - val ticks = VibrationEffect.startComposition() - repeat(config.numberOfLowTicks) { - ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale) + // THEN the correct composition only plays once + assertEquals(/* expected=*/ 1, vibratorHelper.timesVibratedWithEffect(ticks.compose())) } - // GIVEN system running for 1s - clock.advanceTime(1000) - - // WHEN two calls to play occur immediately - sliderHapticFeedbackProvider.onProgress(progress) - sliderHapticFeedbackProvider.onProgress(progress) - - // THEN the correct composition only plays once - verify(vibratorHelper, times(1)) - .vibrate(eq(ticks.compose()), any(VibrationAttributes::class.java)) - } - @Test - fun playHapticAtProgress_beforeNextDragThreshold_playsLowTicksOnce() { - // GIVEN max velocity and a slider progress at half progress - val firstProgress = 0.5f - val firstTicks = generateTicksComposition(config.maxVelocityToScale, firstProgress) - - // Given a second slider progress event smaller than the progress threshold - val secondProgress = firstProgress + max(0f, config.deltaProgressForDragThreshold - 0.01f) - - // GIVEN system running for 1s - clock.advanceTime(1000) - - // WHEN two calls to play occur with the required threshold separation (time and progress) - sliderHapticFeedbackProvider.onProgress(firstProgress) - clock.advanceTime(dragTextureThresholdMillis.toLong()) - sliderHapticFeedbackProvider.onProgress(secondProgress) - - // THEN Only the first compositions plays - verify(vibratorHelper, times(1)) - .vibrate(eq(firstTicks), any(VibrationAttributes::class.java)) - verify(vibratorHelper, times(1)) - .vibrate(any(VibrationEffect::class.java), any(VibrationAttributes::class.java)) - } + fun playHapticAtProgress_beforeNextDragThreshold_playsLowTicksOnce() = + with(kosmos) { + // GIVEN max velocity and a slider progress at half progress + val firstProgress = 0.5f + val firstTicks = generateTicksComposition(config.maxVelocityToScale, firstProgress) + + // Given a second slider progress event smaller than the progress threshold + val secondProgress = + firstProgress + max(0f, config.deltaProgressForDragThreshold - 0.01f) + + // GIVEN system running for 1s + fakeSystemClock.advanceTime(1000) + + // WHEN two calls to play occur with the required threshold separation (time and + // progress) + sliderHapticFeedbackProvider.onProgress(firstProgress) + fakeSystemClock.advanceTime(dragTextureThresholdMillis.toLong()) + sliderHapticFeedbackProvider.onProgress(secondProgress) + + // THEN Only the first compositions plays + assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(firstTicks)) + assertEquals(/* expected= */ 1, vibratorHelper.totalVibrations) + } @Test - fun playHapticAtProgress_afterNextDragThreshold_playsLowTicksTwice() { - // GIVEN max velocity and a slider progress at half progress - val firstProgress = 0.5f - val firstTicks = generateTicksComposition(config.maxVelocityToScale, firstProgress) - - // Given a second slider progress event beyond progress threshold - val secondProgress = firstProgress + config.deltaProgressForDragThreshold + 0.01f - val secondTicks = generateTicksComposition(config.maxVelocityToScale, secondProgress) - - // GIVEN system running for 1s - clock.advanceTime(1000) - - // WHEN two calls to play occur with the required threshold separation (time and progress) - sliderHapticFeedbackProvider.onProgress(firstProgress) - clock.advanceTime(dragTextureThresholdMillis.toLong()) - sliderHapticFeedbackProvider.onProgress(secondProgress) - - // THEN the correct compositions play - verify(vibratorHelper, times(1)) - .vibrate(eq(firstTicks), any(VibrationAttributes::class.java)) - verify(vibratorHelper, times(1)) - .vibrate(eq(secondTicks), any(VibrationAttributes::class.java)) - } + fun playHapticAtProgress_afterNextDragThreshold_playsLowTicksTwice() = + with(kosmos) { + // GIVEN max velocity and a slider progress at half progress + val firstProgress = 0.5f + val firstTicks = generateTicksComposition(config.maxVelocityToScale, firstProgress) + + // Given a second slider progress event beyond progress threshold + val secondProgress = firstProgress + config.deltaProgressForDragThreshold + 0.01f + val secondTicks = generateTicksComposition(config.maxVelocityToScale, secondProgress) + + // GIVEN system running for 1s + fakeSystemClock.advanceTime(1000) + + // WHEN two calls to play occur with the required threshold separation (time and + // progress) + sliderHapticFeedbackProvider.onProgress(firstProgress) + fakeSystemClock.advanceTime(dragTextureThresholdMillis.toLong()) + sliderHapticFeedbackProvider.onProgress(secondProgress) + + // THEN the correct compositions play + assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(firstTicks)) + assertEquals(/* expected= */ 1, vibratorHelper.timesVibratedWithEffect(secondTicks)) + } @Test - fun playHapticAtLowerBookend_afterPlayingAtProgress_playsTwice() { - // GIVEN max velocity and slider progress - val progress = 1f - val expectedScale = - sliderHapticFeedbackProvider.scaleOnDragTexture( - config.maxVelocityToScale, - progress, + fun playHapticAtLowerBookend_afterPlayingAtProgress_playsTwice() = + with(kosmos) { + // GIVEN max velocity and slider progress + val progress = 1f + val expectedScale = + sliderHapticFeedbackProvider.scaleOnDragTexture( + config.maxVelocityToScale, + progress, + ) + val ticks = VibrationEffect.startComposition() + repeat(config.numberOfLowTicks) { + ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale) + } + val bookendVibration = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_CLICK, + sliderHapticFeedbackProvider.scaleOnEdgeCollision( + config.maxVelocityToScale + ), + ) + .compose() + + // GIVEN a vibration at the lower bookend followed by a request to vibrate at progress + sliderHapticFeedbackProvider.onLowerBookend() + sliderHapticFeedbackProvider.onProgress(progress) + + // WHEN a vibration is to trigger again at the lower bookend + sliderHapticFeedbackProvider.onLowerBookend() + + // THEN there are two bookend vibrations + assertEquals( + /* expected= */ 2, + vibratorHelper.timesVibratedWithEffect(bookendVibration) ) - val ticks = VibrationEffect.startComposition() - repeat(config.numberOfLowTicks) { - ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale) } - val bookendVibration = - VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_CLICK, - sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), - ) - .compose() - - // GIVEN a vibration at the lower bookend followed by a request to vibrate at progress - sliderHapticFeedbackProvider.onLowerBookend() - sliderHapticFeedbackProvider.onProgress(progress) - - // WHEN a vibration is to trigger again at the lower bookend - sliderHapticFeedbackProvider.onLowerBookend() - - // THEN there are two bookend vibrations - verify(vibratorHelper, times(2)) - .vibrate(eq(bookendVibration), any(VibrationAttributes::class.java)) - } @Test - fun playHapticAtUpperBookend_afterPlayingAtProgress_playsTwice() { - // GIVEN max velocity and slider progress - val progress = 1f - val expectedScale = - sliderHapticFeedbackProvider.scaleOnDragTexture( - config.maxVelocityToScale, - progress, + fun playHapticAtUpperBookend_afterPlayingAtProgress_playsTwice() = + with(kosmos) { + // GIVEN max velocity and slider progress + val progress = 1f + val expectedScale = + sliderHapticFeedbackProvider.scaleOnDragTexture( + config.maxVelocityToScale, + progress, + ) + val ticks = VibrationEffect.startComposition() + repeat(config.numberOfLowTicks) { + ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale) + } + val bookendVibration = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_CLICK, + sliderHapticFeedbackProvider.scaleOnEdgeCollision( + config.maxVelocityToScale + ), + ) + .compose() + + // GIVEN a vibration at the upper bookend followed by a request to vibrate at progress + sliderHapticFeedbackProvider.onUpperBookend() + sliderHapticFeedbackProvider.onProgress(progress) + + // WHEN a vibration is to trigger again at the upper bookend + sliderHapticFeedbackProvider.onUpperBookend() + + // THEN there are two bookend vibrations + assertEquals( + /* expected= */ 2, + vibratorHelper.timesVibratedWithEffect(bookendVibration) ) - val ticks = VibrationEffect.startComposition() - repeat(config.numberOfLowTicks) { - ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale) } - val bookendVibration = - VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_CLICK, - sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale), - ) - .compose() - - // GIVEN a vibration at the upper bookend followed by a request to vibrate at progress - sliderHapticFeedbackProvider.onUpperBookend() - sliderHapticFeedbackProvider.onProgress(progress) - // WHEN a vibration is to trigger again at the upper bookend - sliderHapticFeedbackProvider.onUpperBookend() + fun dragTextureLastProgress_afterDragTextureHaptics_keepsLastDragTextureProgress() = + with(kosmos) { + // GIVEN max velocity and a slider progress at half progress + val progress = 0.5f - // THEN there are two bookend vibrations - verify(vibratorHelper, times(2)) - .vibrate(eq(bookendVibration), any(VibrationAttributes::class.java)) - } + // GIVEN system running for 1s + fakeSystemClock.advanceTime(1000) - fun dragTextureLastProgress_afterDragTextureHaptics_keepsLastDragTextureProgress() { - // GIVEN max velocity and a slider progress at half progress - val progress = 0.5f + // WHEN a drag texture plays + sliderHapticFeedbackProvider.onProgress(progress) - // GIVEN system running for 1s - clock.advanceTime(1000) - - // WHEN a drag texture plays - sliderHapticFeedbackProvider.onProgress(progress) - - // THEN the dragTextureLastProgress remembers the latest progress - assertEquals(progress, sliderHapticFeedbackProvider.dragTextureLastProgress) - } + // THEN the dragTextureLastProgress remembers the latest progress + assertEquals(progress, sliderHapticFeedbackProvider.dragTextureLastProgress) + } @Test - fun dragTextureLastProgress_afterDragTextureHaptics_resetsOnHandleReleased() { - // GIVEN max velocity and a slider progress at half progress - val progress = 0.5f + fun dragTextureLastProgress_afterDragTextureHaptics_resetsOnHandleReleased() = + with(kosmos) { + // GIVEN max velocity and a slider progress at half progress + val progress = 0.5f - // GIVEN system running for 1s - clock.advanceTime(1000) + // GIVEN system running for 1s + fakeSystemClock.advanceTime(1000) - // WHEN a drag texture plays - sliderHapticFeedbackProvider.onProgress(progress) + // WHEN a drag texture plays + sliderHapticFeedbackProvider.onProgress(progress) - // WHEN the handle is released - sliderHapticFeedbackProvider.onHandleReleasedFromTouch() + // WHEN the handle is released + sliderHapticFeedbackProvider.onHandleReleasedFromTouch() - // THEN the dragTextureLastProgress tracker is reset - assertEquals(-1f, sliderHapticFeedbackProvider.dragTextureLastProgress) - } + // THEN the dragTextureLastProgress tracker is reset + assertEquals(-1f, sliderHapticFeedbackProvider.dragTextureLastProgress) + } private fun generateTicksComposition(velocity: Float, progress: Float): VibrationEffect { val ticks = VibrationEffect.startComposition() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt index 5dd37ae46ee8..66aa572dbc48 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt @@ -131,7 +131,6 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { whenever(clock.smallClock).thenReturn(smallClock) whenever(largeClock.layout).thenReturn(largeClockFaceLayout) whenever(smallClock.layout).thenReturn(smallClockFaceLayout) - whenever(clockViewModel.clock).thenReturn(clock) currentClock.value = clock } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt index 59eb7bb73de7..e56a25345436 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt @@ -66,7 +66,7 @@ private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!! @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper -class MediaDataFilterTest : SysuiTestCase() { +class LegacyMediaDataFilterImplTest : SysuiTestCase() { @Mock private lateinit var listener: MediaDataManager.Listener @Mock private lateinit var userTracker: UserTracker @@ -80,7 +80,7 @@ class MediaDataFilterTest : SysuiTestCase() { @Mock private lateinit var mediaFlags: MediaFlags @Mock private lateinit var cardAction: SmartspaceAction - private lateinit var mediaDataFilter: MediaDataFilter + private lateinit var mediaDataFilter: LegacyMediaDataFilterImpl private lateinit var dataMain: MediaData private lateinit var dataGuest: MediaData private lateinit var dataPrivateProfile: MediaData @@ -92,7 +92,7 @@ class MediaDataFilterTest : SysuiTestCase() { MediaPlayerData.clear() whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) mediaDataFilter = - MediaDataFilter( + LegacyMediaDataFilterImpl( context, userTracker, broadcastSender, @@ -370,7 +370,7 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) mediaDataFilter.onSwipeToDismiss() - verify(mediaDataManager).setTimedOut(eq(KEY), eq(true), eq(true)) + verify(mediaDataManager).setInactive(eq(KEY), eq(true), eq(true)) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index 61bfdb548b4f..5a2d22d0d503 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -114,7 +114,7 @@ private fun <T> anyObject(): T { @SmallTest @RunWithLooper(setAsMainLooper = true) @RunWith(AndroidTestingRunner::class) -class MediaDataManagerTest : SysuiTestCase() { +class LegacyMediaDataManagerImplTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() @Mock lateinit var mediaControllerFactory: MediaControllerFactory @@ -133,7 +133,7 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter @Mock lateinit var mediaDeviceManager: MediaDeviceManager @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest - @Mock lateinit var mediaDataFilter: MediaDataFilter + @Mock lateinit var mediaDataFilter: LegacyMediaDataFilterImpl @Mock lateinit var listener: MediaDataManager.Listener @Mock lateinit var pendingIntent: PendingIntent @Mock lateinit var activityStarter: ActivityStarter @@ -146,7 +146,7 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction @Mock private lateinit var mediaFlags: MediaFlags @Mock private lateinit var logger: MediaUiEventLogger - lateinit var mediaDataManager: MediaDataManager + lateinit var mediaDataManager: LegacyMediaDataManagerImpl lateinit var mediaNotification: StatusBarNotification lateinit var remoteCastNotification: StatusBarNotification @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData> @@ -189,7 +189,7 @@ class MediaDataManagerTest : SysuiTestCase() { 1 ) mediaDataManager = - MediaDataManager( + LegacyMediaDataManagerImpl( context = context, backgroundExecutor = backgroundExecutor, uiExecutor = uiExecutor, @@ -304,13 +304,13 @@ class MediaDataManagerTest : SysuiTestCase() { val data = mediaDataCaptor.value assertThat(data.active).isTrue() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) assertThat(data.active).isFalse() verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) } @Test - fun testSetTimedOut_resume_dismissesMedia() { + fun testsetInactive_resume_dismissesMedia() { // WHEN resume controls are present, and time out val desc = MediaDescription.Builder().run { @@ -339,7 +339,7 @@ class MediaDataManagerTest : SysuiTestCase() { eq(false) ) - mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true) + mediaDataManager.setInactive(PACKAGE_NAME, timedOut = true) verify(logger) .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId)) @@ -1485,7 +1485,7 @@ class MediaDataManagerTest : SysuiTestCase() { // WHEN the notification times out clock.advanceTime(100) val currentTime = clock.elapsedRealtime() - mediaDataManager.setTimedOut(KEY, true, true) + mediaDataManager.setInactive(KEY, true, true) // THEN the last active time is changed verify(listener) @@ -1602,7 +1602,7 @@ class MediaDataManagerTest : SysuiTestCase() { eq(false) ) assertThat(mediaDataCaptor.value.actionsToShowInCompact.size) - .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS) + .isEqualTo(LegacyMediaDataManagerImpl.MAX_COMPACT_ACTIONS) } @Test @@ -1615,7 +1615,7 @@ class MediaDataManagerTest : SysuiTestCase() { modifyNotification(context).also { it.setSmallIcon(android.R.drawable.ic_media_pause) it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) - for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) { + for (i in 0..LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS) { it.addAction(action) } } @@ -1638,7 +1638,7 @@ class MediaDataManagerTest : SysuiTestCase() { eq(false) ) assertThat(mediaDataCaptor.value.actions.size) - .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS) + .isEqualTo(LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS) } @Test @@ -2040,7 +2040,7 @@ class MediaDataManagerTest : SysuiTestCase() { // When a media control based on notification is added, times out, and then removed addNotificationAndLoad() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) assertThat(mediaDataCaptor.value.active).isFalse() mediaDataManager.onNotificationRemoved(KEY) @@ -2070,7 +2070,7 @@ class MediaDataManagerTest : SysuiTestCase() { // When a media control based on notification is added and times out addNotificationAndLoad() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) assertThat(mediaDataCaptor.value.active).isFalse() // and then the session is destroyed @@ -2142,7 +2142,7 @@ class MediaDataManagerTest : SysuiTestCase() { addNotificationAndLoad() val data = mediaDataCaptor.value assertThat(data.active).isTrue() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) mediaDataManager.onNotificationRemoved(KEY) // It remains as a regular player @@ -2162,7 +2162,7 @@ class MediaDataManagerTest : SysuiTestCase() { addNotificationAndLoad() val data = mediaDataCaptor.value assertThat(data.active).isTrue() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) sessionCallbackCaptor.value.invoke(KEY) // It is converted to a resume player @@ -2249,7 +2249,7 @@ class MediaDataManagerTest : SysuiTestCase() { addNotificationAndLoad() val data = mediaDataCaptor.value assertThat(data.active).isTrue() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) sessionCallbackCaptor.value.invoke(KEY) // It is fully removed. diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt new file mode 100644 index 000000000000..564bdc3f5880 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt @@ -0,0 +1,931 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.domain.pipeline + +import android.app.smartspace.SmartspaceAction +import android.os.Bundle +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.ui.controller.MediaPlayerData +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +private const val KEY = "TEST_KEY" +private const val KEY_ALT = "TEST_KEY_2" +private const val USER_MAIN = 0 +private const val USER_GUEST = 10 +private const val PRIVATE_PROFILE = 12 +private const val PACKAGE = "PKG" +private val INSTANCE_ID = InstanceId.fakeInstanceId(123)!! +private const val APP_UID = 99 +private const val SMARTSPACE_KEY = "SMARTSPACE_KEY" +private const val SMARTSPACE_PACKAGE = "SMARTSPACE_PKG" +private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!! + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class MediaDataFilterImplTest : SysuiTestCase() { + + @Mock private lateinit var listener: MediaDataManager.Listener + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var broadcastSender: BroadcastSender + @Mock private lateinit var mediaDataManager: MediaDataManager + @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager + @Mock private lateinit var executor: Executor + @Mock private lateinit var smartspaceData: SmartspaceMediaData + @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction + @Mock private lateinit var logger: MediaUiEventLogger + @Mock private lateinit var mediaFlags: MediaFlags + @Mock private lateinit var cardAction: SmartspaceAction + + private lateinit var mediaDataFilter: MediaDataFilterImpl + private lateinit var mediaFilterRepository: MediaFilterRepository + private lateinit var testScope: TestScope + private lateinit var dataMain: MediaData + private lateinit var dataGuest: MediaData + private lateinit var dataPrivateProfile: MediaData + private val clock = FakeSystemClock() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + MediaPlayerData.clear() + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) + testScope = TestScope() + mediaFilterRepository = MediaFilterRepository() + mediaDataFilter = + MediaDataFilterImpl( + context, + userTracker, + broadcastSender, + lockscreenUserManager, + executor, + clock, + logger, + mediaFlags, + mediaFilterRepository, + ) + mediaDataFilter.mediaDataManager = mediaDataManager + mediaDataFilter.addListener(listener) + + // Start all tests as main user + setUser(USER_MAIN) + + // Set up test media data + dataMain = + MediaTestUtils.emptyMediaData.copy( + userId = USER_MAIN, + packageName = PACKAGE, + instanceId = INSTANCE_ID, + appUid = APP_UID + ) + dataGuest = dataMain.copy(userId = USER_GUEST) + dataPrivateProfile = dataMain.copy(userId = PRIVATE_PROFILE) + + whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY) + whenever(smartspaceData.isActive).thenReturn(true) + whenever(smartspaceData.isValid()).thenReturn(true) + whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE) + whenever(smartspaceData.recommendations) + .thenReturn(listOf(smartspaceMediaRecommendationItem)) + whenever(smartspaceData.headphoneConnectionTimeMillis) + .thenReturn(clock.currentTimeMillis() - 100) + whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID) + whenever(smartspaceData.cardAction).thenReturn(cardAction) + } + + private fun setUser(id: Int) { + whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false) + whenever(lockscreenUserManager.isProfileAvailable(anyInt())).thenReturn(false) + whenever(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true) + whenever(lockscreenUserManager.isProfileAvailable(eq(id))).thenReturn(true) + whenever(lockscreenUserManager.isProfileAvailable(eq(PRIVATE_PROFILE))).thenReturn(true) + mediaDataFilter.handleUserSwitched() + } + + private fun setPrivateProfileUnavailable() { + whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false) + whenever(lockscreenUserManager.isCurrentProfile(eq(USER_MAIN))).thenReturn(true) + whenever(lockscreenUserManager.isCurrentProfile(eq(PRIVATE_PROFILE))).thenReturn(true) + whenever(lockscreenUserManager.isProfileAvailable(eq(PRIVATE_PROFILE))).thenReturn(false) + mediaDataFilter.handleProfileChanged() + } + + @Test + fun testOnDataLoadedForCurrentUser_callsListener() { + // GIVEN a media for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + + // THEN we should tell the listener + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false)) + } + + @Test + fun testOnDataLoadedForGuest_doesNotCallListener() { + // GIVEN a media for guest user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + + // THEN we should NOT tell the listener + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testOnRemovedForCurrent_callsListener() { + // GIVEN a media was removed for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onMediaDataRemoved(KEY) + + // THEN we should tell the listener + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnRemovedForGuest_doesNotCallListener() { + // GIVEN a media was removed for guest user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + mediaDataFilter.onMediaDataRemoved(KEY) + + // THEN we should NOT tell the listener + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnUserSwitched_removesOldUserControls() { + // GIVEN that we have a media loaded for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + + // and we switch to guest user + setUser(USER_GUEST) + + // THEN we should remove the main user's media + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnUserSwitched_addsNewUserControls() { + // GIVEN that we had some media for both users + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataGuest) + reset(listener) + + // and we switch to guest user + setUser(USER_GUEST) + + // THEN we should add back the guest user media + verify(listener) + .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false)) + + // but not the main user's + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testOnProfileChanged_profileUnavailable_loadControls() { + // GIVEN that we had some media for both profiles + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataPrivateProfile) + reset(listener) + + // and we change profile status + setPrivateProfileUnavailable() + + // THEN we should add the private profile media + verify(listener).onMediaDataRemoved(eq(KEY_ALT)) + } + + @Test + fun hasAnyMedia_mediaSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) + + assertThat(hasAnyMedia(selectedUserEntries)).isTrue() + } + + @Test + fun hasAnyMedia_recommendationSet_returnsFalse() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + assertThat(hasAnyMedia(selectedUserEntries)).isFalse() + } + + @Test + fun hasAnyMediaOrRecommendation_mediaSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) + + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isTrue() + } + + @Test + fun hasAnyMediaOrRecommendation_recommendationSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isTrue() + } + + @Test + fun hasActiveMedia_inactiveMediaSet_returnsFalse() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + + val data = dataMain.copy(active = false) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + } + + @Test + fun hasActiveMedia_activeMediaSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val data = dataMain.copy(active = true) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + + assertThat(hasActiveMedia(selectedUserEntries)).isTrue() + } + + @Test + fun hasActiveMediaOrRecommendation_inactiveMediaSet_returnsFalse() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + val data = dataMain.copy(active = false) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + } + + @Test + fun hasActiveMediaOrRecommendation_activeMediaSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + val data = dataMain.copy(active = true) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + } + + @Test + fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(smartspaceData.isActive).thenReturn(false) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + } + + @Test + fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(smartspaceData.isValid()).thenReturn(false) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + } + + @Test + fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(smartspaceData.isActive).thenReturn(true) + whenever(smartspaceData.isValid()).thenReturn(true) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + } + + @Test + fun testHasAnyMediaOrRecommendation_onlyCurrentUser() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isFalse() + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest) + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isFalse() + assertThat(hasAnyMedia(selectedUserEntries)).isFalse() + } + + @Test + fun testHasActiveMediaOrRecommendation_onlyCurrentUser() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + val data = dataGuest.copy(active = true) + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasAnyMedia(selectedUserEntries)).isFalse() + } + + @Test + fun testOnNotificationRemoved_doesNotHaveMedia() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) + mediaDataFilter.onMediaDataRemoved(KEY) + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isFalse() + assertThat(hasAnyMedia(selectedUserEntries)).isFalse() + } + + @Test + fun testOnSwipeToDismiss_setsTimedOut() { + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onSwipeToDismiss() + + verify(mediaDataManager).setInactive(eq(KEY), eq(true), eq(true)) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) + verify(logger, never()).logRecommendationActivated(any(), any(), any()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + + whenever(smartspaceData.isActive).thenReturn(false) + + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(logger, never()).logRecommendationAdded(any(), any()) + verify(logger, never()).logRecommendationActivated(any(), any(), any()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) + clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) + verify(logger, never()).logRecommendationActivated(any(), any(), any()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(smartspaceData.isActive).thenReturn(false) + + val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) + clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(logger, never()).logRecommendationAdded(any(), any()) + verify(logger, never()).logRecommendationActivated(any(), any(), any()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + + whenever(smartspaceData.isActive).thenReturn(false) + + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // AND we get a smartspace signal + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN we should tell listeners to treat the media as not active instead + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean()) + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(logger, never()).logRecommendationAdded(any(), any()) + verify(logger, never()).logRecommendationActivated(any(), any(), any()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(smartspaceData.isValid()).thenReturn(false) + + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // AND we get a smartspace signal + runCurrent() + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN we should tell listeners to treat the media as active instead + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + // Smartspace update shouldn't be propagated for the empty rec list. + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + verify(logger, never()).logRecommendationAdded(any(), any()) + verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // AND we get a smartspace signal + runCurrent() + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN we should tell listeners to treat the media as active instead + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + // Smartspace update should also be propagated but not prioritized. + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) + verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) + } + + @Test + fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + + verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + } + + @Test + fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + runCurrent() + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) + + mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + + verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + } + + @Test + fun testOnSmartspaceLoaded_persistentEnabled_isInactive_notifiesListeners() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + whenever(smartspaceData.isActive).thenReturn(false) + + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isTrue() + } + + @Test + fun testOnSmartspaceLoaded_persistentEnabled_inactive_hasRecentMedia_staysInactive() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + whenever(smartspaceData.isActive).thenReturn(false) + + // If there is media that was recently played but inactive + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // And an inactive recommendation is loaded + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // Smartspace is loaded but the media stays inactive + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isTrue() + } + + @Test + fun testOnSwipeToDismiss_persistentEnabled_recommendationSetInactive() { + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + + val data = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = SMARTSPACE_KEY, + isActive = true, + packageName = SMARTSPACE_PACKAGE, + recommendations = listOf(smartspaceMediaRecommendationItem), + ) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, data) + mediaDataFilter.onSwipeToDismiss() + + verify(mediaDataManager).setRecommendationInactive(eq(SMARTSPACE_KEY)) + verify(mediaDataManager, never()) + .dismissSmartspaceRecommendation(eq(SMARTSPACE_KEY), anyLong()) + } + + @Test + fun testSmartspaceLoaded_shouldTriggerResume_doesTrigger() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // AND we get a smartspace signal with extra to trigger resume + runCurrent() + val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, true) } + whenever(cardAction.extras).thenReturn(extras) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN we should tell listeners to treat the media as active instead + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + // And send the smartspace data, but not prioritized + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + } + + @Test + fun testSmartspaceLoaded_notShouldTriggerResume_doesNotTrigger() { + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // AND we get a smartspace signal with extra to not trigger resume + val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) } + whenever(cardAction.extras).thenReturn(extras) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN listeners are not updated to show media + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true)) + // But the smartspace update is still propagated + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + } + + private fun hasActiveMediaOrRecommendation( + entries: Map<String, MediaData>?, + smartspaceMediaData: SmartspaceMediaData?, + reactivatedKey: String? + ): Boolean { + if (entries == null || smartspaceMediaData == null) { + return false + } + return entries.any { it.value.active } || + (smartspaceMediaData.isActive && + (smartspaceMediaData.isValid() || reactivatedKey != null)) + } + + private fun hasActiveMedia(entries: Map<String, MediaData>?): Boolean { + return entries?.any { it.value.active } ?: false + } + + private fun hasAnyMediaOrRecommendation( + entries: Map<String, MediaData>?, + smartspaceMediaData: SmartspaceMediaData? + ): Boolean { + if (entries == null || smartspaceMediaData == null) { + return false + } + return entries.isNotEmpty() || + (if (mediaFlags.isPersistentSsCardEnabled()) { + smartspaceMediaData.isValid() + } else { + smartspaceMediaData.isActive && smartspaceMediaData.isValid() + }) + } + + private fun hasAnyMedia(entries: Map<String, MediaData>?): Boolean { + return entries?.isNotEmpty() ?: false + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt new file mode 100644 index 000000000000..5c275b454681 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -0,0 +1,2474 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.domain.pipeline + +import android.app.IUriGrantsManager +import android.app.Notification +import android.app.Notification.FLAG_NO_CLEAR +import android.app.Notification.MediaStyle +import android.app.PendingIntent +import android.app.UriGrantsManager +import android.app.smartspace.SmartspaceAction +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceTarget +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.graphics.drawable.Icon +import android.media.MediaDescription +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.service.notification.StatusBarNotification +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import androidx.media.utils.MediaConstants +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.internal.logging.InstanceId +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.InstanceIdSequenceFake +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.data.repository.MediaDataRepository +import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.domain.resume.MediaResumeListener +import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser +import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE +import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.SbnBuilder +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.utils.os.FakeHandler +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoSession +import org.mockito.junit.MockitoJUnit +import org.mockito.quality.Strictness + +private const val KEY = "KEY" +private const val KEY_2 = "KEY_2" +private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" +private const val SMARTSPACE_CREATION_TIME = 1234L +private const val SMARTSPACE_EXPIRY_TIME = 5678L +private const val PACKAGE_NAME = "com.example.app" +private const val SYSTEM_PACKAGE_NAME = "com.android.systemui" +private const val APP_NAME = "SystemUI" +private const val SESSION_ARTIST = "artist" +private const val SESSION_TITLE = "title" +private const val SESSION_BLANK_TITLE = " " +private const val SESSION_EMPTY_TITLE = "" +private const val USER_ID = 0 +private val DISMISS_INTENT = Intent().apply { action = "dismiss" } + +private fun <T> anyObject(): T { + return Mockito.anyObject<T>() +} + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +class MediaDataProcessorTest : SysuiTestCase() { + + @JvmField @Rule val mockito = MockitoJUnit.rule() + @Mock lateinit var mediaControllerFactory: MediaControllerFactory + @Mock lateinit var controller: MediaController + @Mock lateinit var transportControls: MediaController.TransportControls + @Mock lateinit var playbackInfo: MediaController.PlaybackInfo + lateinit var session: MediaSession + private lateinit var metadataBuilder: MediaMetadata.Builder + lateinit var backgroundExecutor: FakeExecutor + private lateinit var foregroundExecutor: FakeExecutor + lateinit var uiExecutor: FakeExecutor + @Mock lateinit var dumpManager: DumpManager + @Mock lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener + @Mock lateinit var mediaResumeListener: MediaResumeListener + @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter + @Mock lateinit var mediaDeviceManager: MediaDeviceManager + @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest + @Mock lateinit var mediaDataFilter: MediaDataFilterImpl + @Mock lateinit var listener: MediaDataManager.Listener + @Mock lateinit var pendingIntent: PendingIntent + @Mock lateinit var activityStarter: ActivityStarter + @Mock lateinit var smartspaceManager: SmartspaceManager + @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + private lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider + @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget + @Mock private lateinit var mediaRecommendationItem: SmartspaceAction + private lateinit var validRecommendationList: List<SmartspaceAction> + @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction + @Mock private lateinit var mediaFlags: MediaFlags + @Mock private lateinit var logger: MediaUiEventLogger + private lateinit var mediaCarouselInteractor: MediaCarouselInteractor + private lateinit var mediaDataProcessor: MediaDataProcessor + private lateinit var mediaNotification: StatusBarNotification + private lateinit var remoteCastNotification: StatusBarNotification + @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData> + private val clock = FakeSystemClock() + @Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit> + @Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit> + @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig> + @Mock private lateinit var ugm: IUriGrantsManager + @Mock private lateinit var imageSource: ImageDecoder.Source + private lateinit var mediaDataRepository: MediaDataRepository + private lateinit var mediaFilterRepository: MediaFilterRepository + private lateinit var testScope: TestScope + private lateinit var testDispatcher: TestDispatcher + private lateinit var testableLooper: TestableLooper + private lateinit var fakeHandler: FakeHandler + + private val settings = FakeSettings() + private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20) + + private val originalSmartspaceSetting = + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 + ) + + private lateinit var staticMockSession: MockitoSession + + @Before + fun setup() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + staticMockSession = + ExtendedMockito.mockitoSession() + .mockStatic<UriGrantsManager>(UriGrantsManager::class.java) + .mockStatic<ImageDecoder>(ImageDecoder::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + whenever(UriGrantsManager.getService()).thenReturn(ugm) + foregroundExecutor = FakeExecutor(clock) + backgroundExecutor = FakeExecutor(clock) + uiExecutor = FakeExecutor(clock) + testableLooper = TestableLooper.get(this) + fakeHandler = FakeHandler(testableLooper.looper) + smartspaceMediaDataProvider = SmartspaceMediaDataProvider() + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 + ) + testDispatcher = UnconfinedTestDispatcher() + testScope = TestScope(testDispatcher) + mediaFilterRepository = MediaFilterRepository() + mediaDataRepository = MediaDataRepository(mediaFlags, dumpManager) + mediaDataProcessor = + MediaDataProcessor( + context = context, + applicationScope = testScope, + backgroundDispatcher = testDispatcher, + backgroundExecutor = backgroundExecutor, + uiExecutor = uiExecutor, + foregroundExecutor = foregroundExecutor, + handler = fakeHandler, + mediaControllerFactory = mediaControllerFactory, + broadcastDispatcher = broadcastDispatcher, + dumpManager = dumpManager, + activityStarter = activityStarter, + smartspaceMediaDataProvider = smartspaceMediaDataProvider, + useMediaResumption = true, + useQsMediaPlayer = true, + systemClock = clock, + secureSettings = settings, + mediaFlags = mediaFlags, + logger = logger, + smartspaceManager = smartspaceManager, + keyguardUpdateMonitor = keyguardUpdateMonitor, + mediaDataRepository = mediaDataRepository, + ) + mediaDataProcessor.start() + mediaCarouselInteractor = + MediaCarouselInteractor( + applicationScope = testScope.backgroundScope, + mediaDataRepository = mediaDataRepository, + mediaDataProcessor = mediaDataProcessor, + mediaTimeoutListener = mediaTimeoutListener, + mediaResumeListener = mediaResumeListener, + mediaSessionBasedFilter = mediaSessionBasedFilter, + mediaDeviceManager = mediaDeviceManager, + mediaDataCombineLatest = mediaDataCombineLatest, + mediaDataFilter = mediaDataFilter, + mediaFilterRepository = mediaFilterRepository, + mediaFlags = mediaFlags + ) + mediaCarouselInteractor.start() + verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor) + verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor) + session = MediaSession(context, "MediaDataProcessorTestSession") + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + } + build() + } + remoteCastNotification = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() + } + metadataBuilder = + MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + } + verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor)) + whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller) + whenever(controller.transportControls).thenReturn(transportControls) + whenever(controller.playbackInfo).thenReturn(playbackInfo) + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) + + // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal + // listeners in the internal processing pipeline. It receives events, but ince it is a + // mock, it doesn't pass those events along the chain to the external listeners. So, just + // treat mediaSessionBasedFilter as a listener for testing. + listener = mediaSessionBasedFilter + + val recommendationExtras = + Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", DISMISS_INTENT) + } + val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play) + whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras) + whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction) + whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras) + whenever(mediaRecommendationItem.icon).thenReturn(icon) + validRecommendationList = + listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem) + whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE) + whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA) + whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) + whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME) + whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false) + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false) + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) + whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false) + whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) + whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false) + } + + @After + fun tearDown() { + staticMockSession.finishMocking() + session.release() + mediaDataProcessor.destroy() + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + originalSmartspaceSetting + ) + } + + @Test + fun testsetInactive_active_deactivatesMedia() { + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + + mediaDataProcessor.setInactive(KEY, timedOut = true) + assertThat(data.active).isFalse() + verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testsetInactive_resume_dismissesMedia() { + // WHEN resume controls are present, and time out + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + mediaDataProcessor.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + + mediaDataProcessor.setInactive(PACKAGE_NAME, timedOut = true) + verify(logger) + .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId)) + + // THEN it is removed and listeners are informed + foregroundExecutor.advanceClockToLast() + foregroundExecutor.runAllReady() + verify(listener).onMediaDataRemoved(PACKAGE_NAME) + } + + @Test + fun testLoadsMetadataOnBackground() { + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.numPending()).isEqualTo(1) + } + + @Test + fun testLoadMetadata_withExplicitIndicator() { + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + .build() + ) + + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isTrue() + } + + @Test + fun testOnMetaDataLoaded_withoutExplicitIndicator() { + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isFalse() + } + + @Test + fun testOnMetaDataLoaded_callsListener() { + addNotificationAndLoad() + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_LOCAL) + ) + } + + @Test + fun testOnMetaDataLoaded_conservesActiveFlag() { + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.active).isTrue() + } + + @Test + fun testOnNotificationAdded_isRcn_markedRemote() { + addNotificationAndLoad(remoteCastNotification) + + assertThat(mediaDataCaptor.value!!.playbackLocation) + .isEqualTo(MediaData.PLAYBACK_CAST_REMOTE) + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(SYSTEM_PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_CAST_REMOTE) + ) + } + + @Test + fun testOnNotificationAdded_hasSubstituteName_isUsed() { + val subName = "Substitute Name" + val notif = + SbnBuilder().run { + modifyNotification(context).also { + it.extras = + Bundle().apply { + putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName) + } + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + } + build() + } + + mediaDataProcessor.onNotificationAdded(KEY, notif) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + + assertThat(mediaDataCaptor.value!!.app).isEqualTo(subName) + } + + @Test + fun testLoadMediaDataInBg_invalidTokenNoCrash() { + val bundle = Bundle() + // wrong data type + bundle.putParcelable(Notification.EXTRA_MEDIA_SESSION, Bundle()) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.addExtras(bundle) + it.setStyle( + MediaStyle().apply { setRemotePlaybackInfo("Remote device", 0, null) } + ) + } + build() + } + + mediaDataProcessor.loadMediaDataInBg(KEY, rcn, null) + // no crash even though the data structure is incorrect + } + + @Test + fun testLoadMediaDataInBg_invalidMediaRemoteIntentNoCrash() { + val bundle = Bundle() + // wrong data type + bundle.putParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT, Bundle()) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.addExtras(bundle) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() + } + + mediaDataProcessor.loadMediaDataInBg(KEY, rcn, null) + // no crash even though the data structure is incorrect + } + + @Test + fun testOnNotificationRemoved_callsListener() { + addNotificationAndLoad() + val data = mediaDataCaptor.value + mediaDataProcessor.onNotificationRemoved(KEY) + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testOnNotificationAdded_emptyTitle_hasPlaceholder() { + // When the manager has a notification with an empty title, and the app is not + // required to include a non-empty title + val mockPackageManager = mock(PackageManager::class.java) + context.setMockPackageManager(mockPackageManager) + whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE) + .build() + ) + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + // Then a media control is created with a placeholder title string + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val placeholderTitle = context.getString(R.string.controls_media_empty_title, APP_NAME) + assertThat(mediaDataCaptor.value.song).isEqualTo(placeholderTitle) + } + + @Test + fun testOnNotificationAdded_blankTitle_hasPlaceholder() { + // GIVEN that the manager has a notification with a blank title, and the app is not + // required to include a non-empty title + val mockPackageManager = mock(PackageManager::class.java) + context.setMockPackageManager(mockPackageManager) + whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE) + .build() + ) + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + // Then a media control is created with a placeholder title string + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val placeholderTitle = context.getString(R.string.controls_media_empty_title, APP_NAME) + assertThat(mediaDataCaptor.value.song).isEqualTo(placeholderTitle) + } + + @Test + fun testOnNotificationAdded_emptyMetadata_usesNotificationTitle() { + // When the app sets the metadata title fields to empty strings, but does include a + // non-blank notification title + val mockPackageManager = mock(PackageManager::class.java) + context.setMockPackageManager(mockPackageManager) + whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE) + .putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, SESSION_EMPTY_TITLE) + .build() + ) + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setContentTitle(SESSION_TITLE) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + } + build() + } + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + // Then the media control is added using the notification's title + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.song).isEqualTo(SESSION_TITLE) + } + + @Test + fun testOnNotificationRemoved_emptyTitle_notConverted() { + // GIVEN that the manager has a notification with a resume action and empty title. + addNotificationAndLoad() + val data = mediaDataCaptor.value + val instanceId = data.instanceId + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded( + KEY, + null, + data.copy(song = SESSION_EMPTY_TITLE, resumeAction = Runnable {}) + ) + + // WHEN the notification is removed + reset(listener) + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN active media is not converted to resume. + verify(listener, never()) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + verify(logger, never()) + .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + } + + @Test + fun testOnNotificationRemoved_blankTitle_notConverted() { + // GIVEN that the manager has a notification with a resume action and blank title. + addNotificationAndLoad() + val data = mediaDataCaptor.value + val instanceId = data.instanceId + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded( + KEY, + null, + data.copy(song = SESSION_BLANK_TITLE, resumeAction = Runnable {}) + ) + + // WHEN the notification is removed + reset(listener) + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN active media is not converted to resume. + verify(listener, never()) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + verify(logger, never()) + .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + } + + @Test + fun testOnNotificationRemoved_withResumption() { + // GIVEN that the manager has a notification with a resume action + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + // WHEN the notification is removed + mediaDataProcessor.onNotificationRemoved(KEY) + // THEN the media data indicates that it is for resumption + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testOnNotificationRemoved_twoWithResumption() { + // GIVEN that the manager has two notifications with resume actions + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + mediaDataProcessor.onNotificationAdded(KEY_2, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(2) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(2) + + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val data = mediaDataCaptor.value + assertThat(data.resumption).isFalse() + + verify(listener) + .onMediaDataLoaded( + eq(KEY_2), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val data2 = mediaDataCaptor.value + assertThat(data2.resumption).isFalse() + + mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + mediaDataProcessor.onMediaDataLoaded(KEY_2, null, data2.copy(resumeAction = Runnable {})) + reset(listener) + // WHEN the first is removed + mediaDataProcessor.onNotificationRemoved(KEY) + // THEN the data is for resumption and the key is migrated to the package name + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + // WHEN the second is removed + mediaDataProcessor.onNotificationRemoved(KEY_2) + // THEN the data is for resumption and the second key is removed + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(PACKAGE_NAME), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + verify(listener).onMediaDataRemoved(eq(KEY_2)) + } + + @Test + fun testOnNotificationRemoved_withResumption_butNotLocal() { + // GIVEN that the manager has a notification with a resume action, but is not local + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) + addNotificationAndLoad() + val data = mediaDataCaptor.value + val dataRemoteWithResume = + data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume) + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_CAST_LOCAL) + ) + + // WHEN the notification is removed + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN the media data is removed + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnNotificationRemoved_withResumption_isRemoteAndRemoteAllowed() { + // With the flag enabled to allow remote media to resume + whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true) + + // GIVEN that the manager has a notification with a resume action, but is not local + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) + addNotificationAndLoad() + val data = mediaDataCaptor.value + val dataRemoteWithResume = + data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume) + + // WHEN the notification is removed + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN the media data is converted to a resume state + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + } + + @Test + fun testOnNotificationRemoved_withResumption_isRcnAndRemoteAllowed() { + // With the flag enabled to allow remote media to resume + whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true) + + // GIVEN that the manager has a remote cast notification + addNotificationAndLoad(remoteCastNotification) + val data = mediaDataCaptor.value + assertThat(data.playbackLocation).isEqualTo(MediaData.PLAYBACK_CAST_REMOTE) + val dataRemoteWithResume = data.copy(resumeAction = Runnable {}) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume) + + // WHEN the RCN is removed + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN the media data is removed + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnNotificationRemoved_withResumption_tooManyPlayers() { + // Given the maximum number of resume controls already + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + for (i in 0..ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { + addResumeControlAndLoad(desc, "$i:$PACKAGE_NAME") + clock.advanceTime(1000) + } + + // And an active, resumable notification + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + + // When the notification is removed + mediaDataProcessor.onNotificationRemoved(KEY) + + // Then it is converted to resumption + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + + // And the oldest resume control was removed + verify(listener).onMediaDataRemoved(eq("0:$PACKAGE_NAME")) + } + + fun testOnNotificationRemoved_lockDownMode() { + whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(true) + + addNotificationAndLoad() + val data = mediaDataCaptor.value + mediaDataProcessor.onNotificationRemoved(KEY) + + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + verify(logger, never()) + .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testAddResumptionControls() { + // WHEN resumption controls are added + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + val currentTime = clock.elapsedRealtime() + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.song).isEqualTo(SESSION_TITLE) + assertThat(data.app).isEqualTo(APP_NAME) + assertThat(data.actions).hasSize(1) + assertThat(data.semanticActions!!.playOrPause).isNotNull() + assertThat(data.lastActive).isAtLeast(currentTime) + verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testAddResumptionControls_withExplicitIndicator() { + val bundle = Bundle() + // WHEN resumption controls are added with explicit indicator + bundle.putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(bundle) + build() + } + val currentTime = clock.elapsedRealtime() + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.song).isEqualTo(SESSION_TITLE) + assertThat(data.app).isEqualTo(APP_NAME) + assertThat(data.actions).hasSize(1) + assertThat(data.semanticActions!!.playOrPause).isNotNull() + assertThat(data.lastActive).isAtLeast(currentTime) + assertThat(data.isExplicit).isTrue() + verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testAddResumptionControls_hasPartialProgress() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added with partial progress + val progress = 0.5 + val extras = + Bundle().apply { + putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + ) + putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress) + } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(extras) + build() + } + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(progress) + } + + @Test + fun testAddResumptionControls_hasNotPlayedProgress() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added that have not been played + val extras = + Bundle().apply { + putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED + ) + } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(extras) + build() + } + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(0) + } + + @Test + fun testAddResumptionControls_hasFullProgress() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added with progress info + val extras = + Bundle().apply { + putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED + ) + } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(extras) + build() + } + addResumeControlAndLoad(desc) + + // THEN the media data includes the progress + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(1) + } + + @Test + fun testAddResumptionControls_hasNoExtras() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added that do not have any extras + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + addResumeControlAndLoad(desc) + + // Resume progress is null + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(null) + } + + @Test + fun testAddResumptionControls_hasEmptyTitle() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added that have empty title + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_EMPTY_TITLE) + build() + } + mediaDataProcessor.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + + // Resumption controls are not added. + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(0) + verify(listener, never()) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } + + @Test + fun testAddResumptionControls_hasBlankTitle() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added that have a blank title + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_BLANK_TITLE) + build() + } + mediaDataProcessor.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + + // Resumption controls are not added. + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(0) + verify(listener, never()) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } + + @Test + fun testResumptionDisabled_dismissesResumeControls() { + // WHEN there are resume controls and resumption is switched off + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + mediaDataProcessor.setMediaResumptionEnabled(false) + + // THEN the resume controls are dismissed + verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testDismissMedia_listenerCalled() { + addNotificationAndLoad() + val data = mediaDataCaptor.value + val removed = mediaDataProcessor.dismissMediaData(KEY, 0L) + assertThat(removed).isTrue() + + foregroundExecutor.advanceClockToLast() + foregroundExecutor.runAllReady() + + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testDismissMedia_keyDoesNotExist_returnsFalse() { + val removed = mediaDataProcessor.dismissMediaData(KEY, 0L) + assertThat(removed).isFalse() + } + + @Test + fun testBadArtwork_doesNotUse() { + // WHEN notification has a too-small artwork + val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setLargeIcon(artwork) + } + build() + } + mediaDataProcessor.onNotificationAdded(KEY, notif) + + // THEN it still loads + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() { + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(logger).getNewInstanceId() + val instanceId = instanceIdSequence.lastInstanceId + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() { + whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf()) + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(logger).getNewInstanceId() + val instanceId = instanceIdSequence.lastInstanceId + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() { + val recommendationExtras = + Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", null) + } + whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras) + whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction) + whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf()) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(logger).getNewInstanceId() + val instanceId = instanceIdSequence.lastInstanceId + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + dismissIntent = null, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() { + smartspaceMediaDataProvider.onTargetsAvailable(listOf()) + verify(logger, never()).getNewInstanceId() + verify(listener, never()) + .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() { + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(logger).getNewInstanceId() + + smartspaceMediaDataProvider.onTargetsAvailable(listOf()) + uiExecutor.advanceClockToLast() + uiExecutor.runAllReady() + + verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false)) + verifyNoMoreInteractions(logger) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() { + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + val instanceId = instanceIdSequence.lastInstanceId + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() { + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + val extras = + Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", DISMISS_INTENT) + putString(EXTRA_KEY_TRIGGER_SOURCE, EXTRA_VALUE_TRIGGER_PERIODIC) + } + whenever(mediaSmartspaceBaseAction.extras).thenReturn(extras) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + val instanceId = instanceIdSequence.lastInstanceId + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = false, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() { + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + val instanceId = instanceIdSequence.lastInstanceId + + smartspaceMediaDataProvider.onTargetsAvailable(listOf()) + uiExecutor.advanceClockToLast() + uiExecutor.runAllReady() + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = false, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + verify(listener, never()).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false)) + } + + @Test + fun testSetRecommendationInactive_notifiesListeners() { + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + val instanceId = instanceIdSequence.lastInstanceId + + mediaDataProcessor.setRecommendationInactive(KEY_MEDIA_SMARTSPACE) + uiExecutor.advanceClockToLast() + uiExecutor.runAllReady() + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = false, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() { + // WHEN media recommendation setting is off + settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + + // THEN smartspace signal is ignored + verify(listener, never()) + .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) + } + + @Test + fun testMediaRecommendationDisabled_removesSmartspaceData() { + // GIVEN a media recommendation card is present + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(listener) + .onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean()) + + // WHEN the media recommendation setting is turned off + settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0) + + // THEN listeners are notified + uiExecutor.advanceClockToLast() + foregroundExecutor.advanceClockToLast() + uiExecutor.runAllReady() + foregroundExecutor.runAllReady() + verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(true)) + } + + @Test + fun testOnMediaDataChanged_updatesLastActiveTime() { + val currentTime = clock.elapsedRealtime() + addNotificationAndLoad() + assertThat(mediaDataCaptor.value!!.lastActive).isAtLeast(currentTime) + } + + @Test + fun testOnMediaDataTimedOut_updatesLastActiveTime() { + // GIVEN that the manager has a notification + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + // WHEN the notification times out + clock.advanceTime(100) + val currentTime = clock.elapsedRealtime() + mediaDataProcessor.setInactive(KEY, timedOut = true, forceUpdate = true) + + // THEN the last active time is changed + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.lastActive).isAtLeast(currentTime) + } + + @Test + fun testOnActiveMediaConverted_updatesLastActiveTime() { + // GIVEN that the manager has a notification with a resume action + addNotificationAndLoad() + val data = mediaDataCaptor.value + val instanceId = data.instanceId + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + + // WHEN the notification is removed + clock.advanceTime(100) + val currentTime = clock.elapsedRealtime() + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN the last active time is changed + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.lastActive).isAtLeast(currentTime) + + // Log as a conversion event, not as a new resume control + verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) + } + + @Test + fun testOnInactiveMediaConverted_doesNotUpdateLastActiveTime() { + // GIVEN that the manager has a notification with a resume action + addNotificationAndLoad() + val data = mediaDataCaptor.value + val instanceId = data.instanceId + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded( + KEY, + null, + data.copy(resumeAction = Runnable {}, active = false) + ) + + // WHEN the notification is removed + clock.advanceTime(100) + val currentTime = clock.elapsedRealtime() + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN the last active time is not changed + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime) + + // Log as a conversion event, not as a new resume control + verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) + } + + @Test + fun testTooManyCompactActions_isTruncated() { + // GIVEN a notification where too many compact actions were specified + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setShowActionsInCompactView(0, 1, 2, 3, 4) + } + ) + } + build() + } + + // WHEN the notification is loaded + mediaDataProcessor.onNotificationAdded(KEY, notif) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + // THEN only the first MAX_COMPACT_ACTIONS are actually set + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.actionsToShowInCompact.size) + .isEqualTo(MediaDataProcessor.MAX_COMPACT_ACTIONS) + } + + @Test + fun testTooManyNotificationActions_isTruncated() { + // GIVEN a notification where too many notification actions are added + val action = Notification.Action(R.drawable.ic_android, "action", null) + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + for (i in 0..MediaDataProcessor.MAX_NOTIFICATION_ACTIONS) { + it.addAction(action) + } + } + build() + } + + // WHEN the notification is loaded + mediaDataProcessor.onNotificationAdded(KEY, notif) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + // THEN only the first MAX_NOTIFICATION_ACTIONS are actually included + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.actions.size) + .isEqualTo(MediaDataProcessor.MAX_NOTIFICATION_ACTIONS) + } + + @Test + fun testPlaybackActions_noState_usesNotification() { + val desc = "Notification Action" + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + whenever(controller.playbackState).thenReturn(null) + + val notifWithAction = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.addAction(android.R.drawable.ic_media_play, desc, null) + } + build() + } + mediaDataProcessor.onNotificationAdded(KEY, notifWithAction) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + + assertThat(mediaDataCaptor.value!!.semanticActions).isNull() + assertThat(mediaDataCaptor.value!!.actions).hasSize(1) + assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc) + } + + @Test + fun testPlaybackActions_hasPrevNext() { + val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val stateActions = + PlaybackState.ACTION_PLAY or + PlaybackState.ACTION_SKIP_TO_PREVIOUS or + PlaybackState.ACTION_SKIP_TO_NEXT + val stateBuilder = PlaybackState.Builder().setActions(stateActions) + customDesc.forEach { + stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) + } + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + + addNotificationAndLoad() + + assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull() + val actions = mediaDataCaptor.value!!.semanticActions!! + + assertThat(actions.playOrPause).isNotNull() + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) + actions.playOrPause!!.action!!.run() + verify(transportControls).play() + + assertThat(actions.prevOrCustom).isNotNull() + assertThat(actions.prevOrCustom!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_prev)) + actions.prevOrCustom!!.action!!.run() + verify(transportControls).skipToPrevious() + + assertThat(actions.nextOrCustom).isNotNull() + assertThat(actions.nextOrCustom!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_next)) + actions.nextOrCustom!!.action!!.run() + verify(transportControls).skipToNext() + + assertThat(actions.custom0).isNotNull() + assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[0]) + + assertThat(actions.custom1).isNotNull() + assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1]) + } + + @Test + fun testPlaybackActions_noPrevNext_usesCustom() { + val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5") + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val stateActions = PlaybackState.ACTION_PLAY + val stateBuilder = PlaybackState.Builder().setActions(stateActions) + customDesc.forEach { + stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) + } + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + + addNotificationAndLoad() + + assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull() + val actions = mediaDataCaptor.value!!.semanticActions!! + + assertThat(actions.playOrPause).isNotNull() + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) + + assertThat(actions.prevOrCustom).isNotNull() + assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(customDesc[0]) + + assertThat(actions.nextOrCustom).isNotNull() + assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo(customDesc[1]) + + assertThat(actions.custom0).isNotNull() + assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[2]) + + assertThat(actions.custom1).isNotNull() + assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3]) + } + + @Test + fun testPlaybackActions_connecting() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val stateActions = PlaybackState.ACTION_PLAY + val stateBuilder = + PlaybackState.Builder() + .setState(PlaybackState.STATE_BUFFERING, 0, 10f) + .setActions(stateActions) + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + + addNotificationAndLoad() + + assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull() + val actions = mediaDataCaptor.value!!.semanticActions!! + + assertThat(actions.playOrPause).isNotNull() + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_connecting)) + } + + @Test + fun testPlaybackActions_reservedSpace() { + val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val stateActions = PlaybackState.ACTION_PLAY + val stateBuilder = PlaybackState.Builder().setActions(stateActions) + customDesc.forEach { + stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) + } + val extras = + Bundle().apply { + putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) + putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) + } + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + whenever(controller.extras).thenReturn(extras) + + addNotificationAndLoad() + + assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull() + val actions = mediaDataCaptor.value!!.semanticActions!! + + assertThat(actions.playOrPause).isNotNull() + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) + + assertThat(actions.prevOrCustom).isNull() + assertThat(actions.nextOrCustom).isNull() + + assertThat(actions.custom0).isNotNull() + assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[0]) + + assertThat(actions.custom1).isNotNull() + assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1]) + + assertThat(actions.reserveNext).isTrue() + assertThat(actions.reservePrev).isTrue() + } + + @Test + fun testPlaybackActions_playPause_hasButton() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val stateActions = PlaybackState.ACTION_PLAY_PAUSE + val stateBuilder = PlaybackState.Builder().setActions(stateActions) + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + + addNotificationAndLoad() + + assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull() + val actions = mediaDataCaptor.value!!.semanticActions!! + + assertThat(actions.playOrPause).isNotNull() + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) + actions.playOrPause!!.action!!.run() + verify(transportControls).play() + } + + @Test + fun testPlaybackLocationChange_isLogged() { + // Media control added for local playback + addNotificationAndLoad() + val instanceId = mediaDataCaptor.value.instanceId + + // Location is updated to local cast + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) + addNotificationAndLoad() + verify(logger) + .logPlaybackLocationChange( + anyInt(), + eq(PACKAGE_NAME), + eq(instanceId), + eq(MediaData.PLAYBACK_CAST_LOCAL) + ) + + // update to remote cast + mediaDataProcessor.onNotificationAdded(KEY, remoteCastNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(logger) + .logPlaybackLocationChange( + anyInt(), + eq(SYSTEM_PACKAGE_NAME), + eq(instanceId), + eq(MediaData.PLAYBACK_CAST_REMOTE) + ) + } + + @Test + fun testPlaybackStateChange_keyExists_callsListener() { + // Notification has been added + addNotificationAndLoad() + + // Callback gets an updated state + val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() + stateCallbackCaptor.value.invoke(KEY, state) + + // Listener is notified of updated state + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.isPlaying).isTrue() + } + + @Test + fun testPlaybackStateChange_keyDoesNotExist_doesNothing() { + val state = PlaybackState.Builder().build() + + // No media added with this key + + stateCallbackCaptor.value.invoke(KEY, state) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testPlaybackStateChange_keyHasNullToken_doesNothing() { + // When we get an update that sets the data's token to null + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(token = null)) + + // And then get a state update + val state = PlaybackState.Builder().build() + + // Then no changes are made + stateCallbackCaptor.value.invoke(KEY, state) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val state = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 1f).build() + whenever(controller.playbackState).thenReturn(state) + + addNotificationAndLoad() + stateCallbackCaptor.value.invoke(KEY, state) + + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + assertThat(mediaDataCaptor.value.semanticActions).isNotNull() + } + + @Test + fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() { + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + val state = + PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 1f) + .setActions(PlaybackState.ACTION_PLAY_PAUSE) + .build() + + // Add resumption controls in order to have semantic actions. + // To make sure that they are not null after changing state. + mediaDataProcessor.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + + stateCallbackCaptor.value.invoke(PACKAGE_NAME, state) + + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(PACKAGE_NAME), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + assertThat(mediaDataCaptor.value.semanticActions).isNotNull() + } + + @Test + fun testPlaybackStateNull_Pause_keyExists_callsListener() { + whenever(controller.playbackState).thenReturn(null) + val state = + PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 1f) + .setActions(PlaybackState.ACTION_PLAY_PAUSE) + .build() + + addNotificationAndLoad() + stateCallbackCaptor.value.invoke(KEY, state) + + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + assertThat(mediaDataCaptor.value.semanticActions).isNull() + } + + @Test + fun testNoClearNotOngoing_canDismiss() { + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setOngoing(false) + it.setFlag(FLAG_NO_CLEAR, true) + } + build() + } + addNotificationAndLoad() + assertThat(mediaDataCaptor.value.isClearable).isTrue() + } + + @Test + fun testOngoing_cannotDismiss() { + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setOngoing(true) + } + build() + } + addNotificationAndLoad() + assertThat(mediaDataCaptor.value.isClearable).isFalse() + } + + @Test + fun testRetain_notifPlayer_notifRemoved_setToResume() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + + // When a media control based on notification is added, times out, and then removed + addNotificationAndLoad() + mediaDataProcessor.setInactive(KEY, timedOut = true) + assertThat(mediaDataCaptor.value.active).isFalse() + mediaDataProcessor.onNotificationRemoved(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test + fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + + // When a media control based on notification is added and times out + addNotificationAndLoad() + mediaDataProcessor.setInactive(KEY, timedOut = true) + assertThat(mediaDataCaptor.value.active).isFalse() + + // and then the session is destroyed + sessionCallbackCaptor.value.invoke(KEY) + + // It remains as a regular player + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + verify(listener, never()) + .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + + // When a media control based on notification is added and then removed, without timing out + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + mediaDataProcessor.onNotificationRemoved(KEY) + + // It is fully removed + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(listener, never()) + .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testRetain_canResume_removeWhileActive_setToResume() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + + // When a media control that supports resumption is added + addNotificationAndLoad() + val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {}) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable) + + // And then removed while still active + mediaDataProcessor.onNotificationRemoved(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test + fun testRetain_sessionPlayer_notifRemoved_doesNotChange() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control with PlaybackState actions is added, times out, + // and then the notification is removed + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + mediaDataProcessor.setInactive(KEY, timedOut = true) + mediaDataProcessor.onNotificationRemoved(KEY) + + // It remains as a regular player + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + verify(listener, never()) + .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testRetain_sessionPlayer_sessionDestroyed_setToResume() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control with PlaybackState actions is added, times out, + // and then the session is destroyed + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + mediaDataProcessor.setInactive(KEY, timedOut = true) + sessionCallbackCaptor.value.invoke(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test + fun testRetain_sessionPlayer_destroyedWhileActive_noResume_fullyRemoved() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control using session actions is added, and then the session is destroyed + // without timing out first + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + sessionCallbackCaptor.value.invoke(KEY) + + // It is fully removed + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(listener, never()) + .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testRetain_sessionPlayer_canResume_destroyedWhileActive_setToResume() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control using session actions and that does allow resumption is added, + addNotificationAndLoad() + val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {}) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable) + + // And then the session is destroyed without timing out first + sessionCallbackCaptor.value.invoke(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test + fun testSessionPlayer_sessionDestroyed_noResume_fullyRemoved() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control with PlaybackState actions is added, times out, + // and then the session is destroyed + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + mediaDataProcessor.setInactive(KEY, timedOut = true) + sessionCallbackCaptor.value.invoke(KEY) + + // It is fully removed. + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(listener, never()) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } + + @Test + fun testSessionPlayer_destroyedWhileActive_noResume_fullyRemoved() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control using session actions is added, and then the session is destroyed + // without timing out first + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + sessionCallbackCaptor.value.invoke(KEY) + + // It is fully removed + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(listener, never()) + .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testSessionPlayer_canResume_destroyedWhileActive_setToResume() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control using session actions and that does allow resumption is added, + addNotificationAndLoad() + val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {}) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable) + + // And then the session is destroyed without timing out first + sessionCallbackCaptor.value.invoke(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test + fun testSessionDestroyed_noNotificationKey_stillRemoved() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + + // When a notiifcation is added and then removed before it is fully processed + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + backgroundExecutor.runAllReady() + mediaDataProcessor.onNotificationRemoved(KEY) + + // We still make sure to remove it + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testResumeMediaLoaded_hasArtPermission_artLoaded() { + // When resume media is loaded and user/app has permission to access the art URI, + whenever( + ugm.checkGrantUriPermission_ignoreNonSystem( + anyInt(), + any(), + any(), + anyInt(), + anyInt() + ) + ) + .thenReturn(1) + val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + val uri = Uri.parse("content://example") + whenever(ImageDecoder.createSource(any(), eq(uri))).thenReturn(imageSource) + whenever(ImageDecoder.decodeBitmap(any(), any())).thenReturn(artwork) + + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setIconUri(uri) + build() + } + addResumeControlAndLoad(desc) + + // Then the artwork is loaded + assertThat(mediaDataCaptor.value.artwork).isNotNull() + } + + @Test + fun testResumeMediaLoaded_noArtPermission_noArtLoaded() { + // When resume media is loaded and user/app does not have permission to access the art URI + whenever( + ugm.checkGrantUriPermission_ignoreNonSystem( + anyInt(), + any(), + any(), + anyInt(), + anyInt() + ) + ) + .thenThrow(SecurityException("Test no permission")) + val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + val uri = Uri.parse("content://example") + whenever(ImageDecoder.createSource(any(), eq(uri))).thenReturn(imageSource) + whenever(ImageDecoder.decodeBitmap(any(), any())).thenReturn(artwork) + + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setIconUri(uri) + build() + } + addResumeControlAndLoad(desc) + + // Then the artwork is not loaded + assertThat(mediaDataCaptor.value.artwork).isNull() + } + + /** Helper function to add a basic media notification and capture the resulting MediaData */ + private fun addNotificationAndLoad() { + addNotificationAndLoad(mediaNotification) + } + + /** Helper function to add the given notification and capture the resulting MediaData */ + private fun addNotificationAndLoad(sbn: StatusBarNotification) { + mediaDataProcessor.onNotificationAdded(KEY, sbn) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } + + /** Helper function to set up a PlaybackState with action */ + private fun addPlaybackStateAction() { + val stateActions = PlaybackState.ACTION_PLAY_PAUSE + val stateBuilder = PlaybackState.Builder().setActions(stateActions) + stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f) + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + } + + /** Helper function to add a resumption control and capture the resulting MediaData */ + private fun addResumeControlAndLoad( + desc: MediaDescription, + packageName: String = PACKAGE_NAME + ) { + mediaDataProcessor.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + packageName + ) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + verify(listener) + .onMediaDataLoaded( + eq(packageName), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt index 7f3d79f7e288..a447e442a384 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt @@ -41,7 +41,6 @@ import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.settingslib.media.PhoneMediaDevice import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDeviceData @@ -98,7 +97,6 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Mock private lateinit var muteAwaitManager: MediaMuteAwaitConnectionManager private lateinit var fakeFgExecutor: FakeExecutor private lateinit var fakeBgExecutor: FakeExecutor - @Mock private lateinit var dumpster: DumpManager @Mock private lateinit var listener: MediaDeviceManager.Listener @Mock private lateinit var device: MediaDevice @Mock private lateinit var icon: Drawable @@ -133,7 +131,6 @@ public class MediaDeviceManagerTest : SysuiTestCase() { { localBluetoothManager }, fakeFgExecutor, fakeBgExecutor, - dumpster, ) manager.addListener(listener) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt index 9f5260c252e4..37dea11ccaaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaControllerTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.media.controls.ui.controller -import android.provider.Settings import android.test.suitebuilder.annotation.SmallTest import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -37,8 +36,6 @@ import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.util.animation.UniqueObjectHostView 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.utils.os.FakeHandler import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertTrue import org.junit.Before @@ -65,10 +62,7 @@ class KeyguardMediaControllerTest : SysuiTestCase() { private val mediaContainerView: MediaContainerView = MediaContainerView(context, null) private val hostView = UniqueObjectHostView(context) - private val settings = FakeSettings() private lateinit var keyguardMediaController: KeyguardMediaController - private lateinit var testableLooper: TestableLooper - private lateinit var fakeHandler: FakeHandler private lateinit var statusBarStateListener: StatusBarStateController.StateListener @Before @@ -84,16 +78,12 @@ class KeyguardMediaControllerTest : SysuiTestCase() { whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) whenever(mediaHost.hostView).thenReturn(hostView) hostView.layoutParams = FrameLayout.LayoutParams(100, 100) - testableLooper = TestableLooper.get(this) - fakeHandler = FakeHandler(testableLooper.looper) keyguardMediaController = KeyguardMediaController( mediaHost, bypassController, statusBarStateController, context, - settings, - fakeHandler, configurationController, ResourcesSplitShadeStateController(), mock<KeyguardMediaControllerLogger>(), @@ -126,24 +116,6 @@ class KeyguardMediaControllerTest : SysuiTestCase() { } @Test - fun testHiddenOnKeyguard_whenMediaOnLockScreenDisabled() { - settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 0) - - keyguardMediaController.refreshMediaPosition(TEST_REASON) - - assertThat(mediaContainerView.visibility).isEqualTo(GONE) - } - - @Test - fun testAvailableOnKeyguard_whenMediaOnLockScreenEnabled() { - settings.putInt(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, 1) - - keyguardMediaController.refreshMediaPosition(TEST_REASON) - - assertThat(mediaContainerView.visibility).isEqualTo(VISIBLE) - } - - @Test fun testActivatesSplitShadeContainerInSplitShadeMode() { val splitShadeContainer = FrameLayout(context) keyguardMediaController.attachSplitShadeContainer(splitShadeContainer) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt index f755199b4c72..c3daf8485634 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt @@ -34,14 +34,13 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData -import com.android.systemui.media.controls.shared.model.SmartspaceMediaData import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.controls.ui.view.MediaScrollView @@ -60,7 +59,9 @@ import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq +import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.time.FakeSystemClock import java.util.Locale import javax.inject.Provider @@ -68,6 +69,7 @@ import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before @@ -111,13 +113,12 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var logger: MediaUiEventLogger @Mock lateinit var debugLogger: MediaCarouselControllerLogger @Mock lateinit var mediaViewController: MediaViewController - @Mock lateinit var smartspaceMediaData: SmartspaceMediaData @Mock lateinit var mediaCarousel: MediaScrollView @Mock lateinit var pageIndicator: PageIndicator @Mock lateinit var mediaFlags: MediaFlags @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock lateinit var globalSettings: GlobalSettings + private lateinit var secureSettings: SecureSettings private val transitionRepository = kosmos.fakeKeyguardTransitionRepository @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener> @Captor @@ -129,13 +130,16 @@ class MediaCarouselControllerTest : SysuiTestCase() { private val clock = FakeSystemClock() private lateinit var bgExecutor: FakeExecutor + private lateinit var testDispatcher: TestDispatcher private lateinit var mediaCarouselController: MediaCarouselController @Before fun setup() { MockitoAnnotations.initMocks(this) + secureSettings = FakeSettings() context.resources.configuration.setLocales(LocaleList(Locale.US, Locale.UK)) bgExecutor = FakeExecutor(clock) + testDispatcher = UnconfinedTestDispatcher() mediaCarouselController = MediaCarouselController( context, @@ -146,6 +150,7 @@ class MediaCarouselControllerTest : SysuiTestCase() { clock, executor, bgExecutor, + testDispatcher, mediaDataManager, configurationController, falsingManager, @@ -155,7 +160,8 @@ class MediaCarouselControllerTest : SysuiTestCase() { mediaFlags, keyguardUpdateMonitor, kosmos.keyguardTransitionInteractor, - globalSettings + globalSettings, + secureSettings, ) verify(configurationController).addCallback(capture(configListener)) verify(mediaDataManager).addListener(capture(listener)) @@ -165,7 +171,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { verify(mediaHostStatesManager).addCallback(capture(hostStateCallback)) whenever(mediaControlPanelFactory.get()).thenReturn(panel) whenever(panel.mediaViewController).thenReturn(mediaViewController) - whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData) whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) MediaPlayerData.clear() verify(globalSettings) @@ -810,7 +815,9 @@ class MediaCarouselControllerTest : SysuiTestCase() { @ExperimentalCoroutinesApi @Test fun testKeyguardGone_showMediaCarousel() = - runTest(UnconfinedTestDispatcher()) { + kosmos.testScope.runTest { + var updatedVisibility = false + mediaCarouselController.updateHostVisibility = { updatedVisibility = true } mediaCarouselController.mediaCarousel = mediaCarousel val job = mediaCarouselController.listenForAnyStateToGoneKeyguardTransition(this) @@ -821,10 +828,64 @@ class MediaCarouselControllerTest : SysuiTestCase() { ) verify(mediaCarousel).visibility = View.VISIBLE + assertEquals(true, updatedVisibility) + assertEquals(false, mediaCarouselController.isLockedAndHidden()) job.cancel() } + @ExperimentalCoroutinesApi + @Test + fun keyguardShowing_notAllowedOnLockscreen_updateVisibility() { + kosmos.testScope.runTest { + var updatedVisibility = false + mediaCarouselController.updateHostVisibility = { updatedVisibility = true } + mediaCarouselController.mediaCarousel = mediaCarousel + + val settingsJob = mediaCarouselController.listenForLockscreenSettingChanges(this) + secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, false) + + val keyguardJob = mediaCarouselController.listenForAnyStateToLockscreenTransition(this) + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this + ) + + assertEquals(true, updatedVisibility) + assertEquals(true, mediaCarouselController.isLockedAndHidden()) + + settingsJob.cancel() + keyguardJob.cancel() + } + } + + @ExperimentalCoroutinesApi + @Test + fun keyguardShowing_allowedOnLockscreen_updateVisibility() { + kosmos.testScope.runTest { + var updatedVisibility = false + mediaCarouselController.updateHostVisibility = { updatedVisibility = true } + mediaCarouselController.mediaCarousel = mediaCarousel + + val settingsJob = mediaCarouselController.listenForLockscreenSettingChanges(this) + secureSettings.putBool(Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, true) + + val keyguardJob = mediaCarouselController.listenForAnyStateToLockscreenTransition(this) + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + this + ) + + assertEquals(true, updatedVisibility) + assertEquals(false, mediaCarouselController.isLockedAndHidden()) + + settingsJob.cancel() + keyguardJob.cancel() + } + } + @Test fun testInvisibleToUserAndExpanded_playersNotListening() { // Add players to carousel. diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt index aa54565c2aa0..6e0919f5f1d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt @@ -28,9 +28,10 @@ import android.view.MotionEvent.ACTION_UP import android.view.ViewConfiguration import android.view.WindowManager import androidx.test.filters.SmallTest -import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker import com.android.systemui.SysuiTestCase +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController @@ -41,10 +42,8 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock -import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -62,16 +61,13 @@ class BackPanelControllerTest : SysuiTestCase() { @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var latencyTracker: LatencyTracker - @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor + private val interactionJankMonitor = Kosmos().interactionJankMonitor @Mock private lateinit var layoutParams: WindowManager.LayoutParams @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback @Before fun setup() { MockitoAnnotations.initMocks(this) - `when`(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true) - `when`(interactionJankMonitor.end(anyInt())).thenReturn(true) - `when`(interactionJankMonitor.cancel(anyInt())).thenReturn(true) mBackPanelController = BackPanelController( context, diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java index a63b2211f71a..db0c0bcfa8f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java @@ -80,6 +80,8 @@ import android.app.people.IPeopleManager; import android.app.people.PeopleManager; import android.app.people.PeopleSpaceTile; import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -101,11 +103,12 @@ import android.text.TextUtils; import androidx.preference.PreferenceManager; import androidx.test.filters.SmallTest; -import com.android.systemui.res.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.people.PeopleBackupFollowUpJob; import com.android.systemui.people.PeopleSpaceUtils; import com.android.systemui.people.SharedPreferencesHelper; +import com.android.systemui.res.R; +import com.android.systemui.settings.FakeUserTracker; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationListener.NotificationHandler; import com.android.systemui.statusbar.SbnBuilder; @@ -265,6 +268,8 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { private final FakeExecutor mFakeExecutor = new FakeExecutor(mClock); + private final FakeUserTracker mUserTracker = new FakeUserTracker(); + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -272,7 +277,7 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { mManager = new PeopleSpaceWidgetManager(mContext, mAppWidgetManager, mIPeopleManager, mPeopleManager, mLauncherApps, mNotifCollection, mPackageManager, Optional.of(mBubbles), mUserManager, mBackupManager, mINotificationManager, - mNotificationManager, mFakeExecutor); + mNotificationManager, mFakeExecutor, mUserTracker); mManager.attach(mListenerService); verify(mListenerService).addNotificationHandler(mListenerCaptor.capture()); @@ -309,6 +314,12 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { .setId(1) .setShortcutInfo(mShortcutInfo) .build(); + + AppWidgetProviderInfo providerInfo = new AppWidgetProviderInfo(); + providerInfo.provider = new ComponentName("com.android.systemui.tests", + "com.android.systemui.people.widget.PeopleSpaceWidgetProvider"); + when(mAppWidgetManager.getInstalledProvidersForPackage(anyString(), any())) + .thenReturn(List.of(providerInfo)); } @Test @@ -1562,6 +1573,43 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { String.valueOf(WIDGET_ID_WITH_KEY_IN_OPTIONS)); } + @Test + public void testUpdateGeneratedPreview_flagDisabled() { + mSetFlagsRule.disableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); + verify(mAppWidgetManager, times(0)).setWidgetPreview(any(), anyInt(), any()); + } + + @Test + public void testUpdateGeneratedPreview_userLocked() { + mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(false); + + mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); + verify(mAppWidgetManager, times(0)).setWidgetPreview(any(), anyInt(), any()); + } + + @Test + public void testUpdateGeneratedPreview_userUnlocked() { + mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true); + when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true); + + mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); + verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any()); + } + + @Test + public void testUpdateGeneratedPreview_doesNotSetTwice() { + mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS); + when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true); + when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true); + + mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); + mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle()); + verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any()); + } + private void setFinalField(String fieldName, int value) { try { Field field = NotificationManager.Policy.class.getDeclaredField(fieldName); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt index cc48640b15bc..5c6ed70c85a6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt @@ -21,6 +21,7 @@ import android.testing.TestableLooper.RunWithLooper import android.testing.ViewUtils import android.view.ContextThemeWrapper import android.view.View +import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.accessibility.AccessibilityNodeInfo import android.widget.FrameLayout @@ -71,7 +72,7 @@ class QSPanelTest : SysuiTestCase() { qsPanel = QSPanel(themedContext, null) qsPanel.mUsingMediaPlayer = true - qsPanel.initialize(qsLogger) + qsPanel.initialize(qsLogger, true) // QSPanel inflates a footer inside of it, mocking it here footer = LinearLayout(themedContext).apply { id = R.id.qs_footer } qsPanel.addView(footer, MATCH_PARENT, 100) @@ -218,6 +219,62 @@ class QSPanelTest : SysuiTestCase() { verify(tile).addCallback(record.callback) } + @Test + fun initializedWithNoMedia_tileLayoutParentIsAlwaysQsPanel() { + lateinit var panel: QSPanel + lateinit var tileLayout: View + testableLooper.runWithLooper { + panel = QSPanel(themedContext, null) + panel.mUsingMediaPlayer = true + + panel.initialize(qsLogger, /* usingMediaPlayer= */ false) + tileLayout = panel.orCreateTileLayout as View + // QSPanel inflates a footer inside of it, mocking it here + footer = LinearLayout(themedContext).apply { id = R.id.qs_footer } + panel.addView(footer, MATCH_PARENT, 100) + panel.onFinishInflate() + // Provides a parent with non-zero size for QSPanel + ViewUtils.attachView(panel) + } + val mockMediaHost = mock(ViewGroup::class.java) + + panel.setUsingHorizontalLayout(false, mockMediaHost, true) + + assertThat(tileLayout.parent).isSameInstanceAs(panel) + + panel.setUsingHorizontalLayout(true, mockMediaHost, true) + assertThat(tileLayout.parent).isSameInstanceAs(panel) + + ViewUtils.detachView(panel) + } + + @Test + fun initializeWithNoMedia_mediaNeverAttached() { + lateinit var panel: QSPanel + testableLooper.runWithLooper { + panel = QSPanel(themedContext, null) + panel.mUsingMediaPlayer = true + + panel.initialize(qsLogger, /* usingMediaPlayer= */ false) + panel.orCreateTileLayout as View + // QSPanel inflates a footer inside of it, mocking it here + footer = LinearLayout(themedContext).apply { id = R.id.qs_footer } + panel.addView(footer, MATCH_PARENT, 100) + panel.onFinishInflate() + // Provides a parent with non-zero size for QSPanel + ViewUtils.attachView(panel) + } + val mockMediaHost = FrameLayout(themedContext) + + panel.setUsingHorizontalLayout(false, mockMediaHost, true) + assertThat(mockMediaHost.parent).isNull() + + panel.setUsingHorizontalLayout(true, mockMediaHost, true) + assertThat(mockMediaHost.parent).isNull() + + ViewUtils.detachView(panel) + } + private infix fun View.isLeftOf(other: View): Boolean { val rect = Rect() getBoundsOnScreen(rect) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt index 3fba3938db19..e5369fcae0b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt @@ -36,7 +36,7 @@ class QuickQSPanelTest : SysuiTestCase() { testableLooper.runWithLooper { quickQSPanel = QuickQSPanel(mContext, null) - quickQSPanel.initialize(qsLogger) + quickQSPanel.initialize(qsLogger, true) quickQSPanel.onFinishInflate() // Provides a parent with non-zero size for QSPanel diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt index e0fff9c10873..04e214ac7a04 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSTileViewImplTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.qs.tileimpl import android.content.Context import android.graphics.drawable.Drawable +import android.platform.test.annotations.EnableFlags import android.service.quicksettings.Tile import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -27,6 +28,7 @@ import android.view.View import android.view.accessibility.AccessibilityNodeInfo import android.widget.TextView import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.qs.QSTile @@ -380,6 +382,34 @@ class QSTileViewImplTest : SysuiTestCase() { assertThat(tileView.stateDescription?.contains(unavailableString)).isTrue() } + @Test + @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS) + fun onStateChange_longPressEffectActive_withInvalidDuration_doesNotCreateEffect() { + val state = QSTile.State() // A state that handles longPress + + // GIVEN an invalid long-press effect duration + tileView.constantLongPressEffectDuration = -1 + + // WHEN the state changes + tileView.changeState(state) + + // THEN the long-press effect is not created + assertThat(tileView.hasLongPressEffect).isFalse() + } + + @Test + @EnableFlags(FLAG_QUICK_SETTINGS_VISUAL_HAPTICS_LONGPRESS) + fun onStateChange_longPressEffectActive_withValidDuration_createsEffect() { + // GIVEN a test state that handles long-press and a valid long-press effect duration + val state = QSTile.State() + + // WHEN the state changes + tileView.changeState(state) + + // THEN the long-press effect created + assertThat(tileView.hasLongPressEffect).isTrue() + } + class FakeTileView( context: Context, collapsed: Boolean @@ -387,6 +417,9 @@ class QSTileViewImplTest : SysuiTestCase() { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings), collapsed ) { + var constantLongPressEffectDuration = 500 + + override fun getLongPressEffectDuration(): Int = constantLongPressEffectDuration fun changeState(state: QSTile.State) { handleStateChanged(state) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt index 761c411bdcb8..37654d515a21 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.qs.QSHost import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor +import com.android.systemui.recordissue.IssueRecordingState import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.res.R import com.android.systemui.settings.UserContextProvider @@ -74,6 +75,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Mock private lateinit var dialog: SystemUIDialog private lateinit var testableLooper: TestableLooper + private val issueRecordingState = IssueRecordingState() private lateinit var tile: RecordIssueTile @Before @@ -100,13 +102,14 @@ class RecordIssueTileTest : SysuiTestCase() { dialogLauncherAnimator, panelInteractor, userContextProvider, + issueRecordingState, delegateFactory, ) } @Test fun qsTileUi_shouldLookCorrect_whenInactive() { - tile.isRecording = false + issueRecordingState.isRecording = false val testState = tile.newTileState() tile.handleUpdateState(testState, null) @@ -118,8 +121,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Test fun qsTileUi_shouldLookCorrect_whenRecording() { - tile.isRecording = true - + issueRecordingState.isRecording = true val testState = tile.newTileState() tile.handleUpdateState(testState, null) @@ -130,7 +132,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Test fun inActiveQsTile_switchesToActive_whenClicked() { - tile.isRecording = false + issueRecordingState.isRecording = false val testState = tile.newTileState() tile.handleUpdateState(testState, null) @@ -140,7 +142,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Test fun activeQsTile_switchesToInActive_whenClicked() { - tile.isRecording = true + issueRecordingState.isRecording = true val testState = tile.newTileState() tile.handleUpdateState(testState, null) @@ -150,7 +152,8 @@ class RecordIssueTileTest : SysuiTestCase() { @Test fun showPrompt_shouldUseKeyguardDismissUtil_ToShowDialog() { - tile.isRecording = false + issueRecordingState.isRecording = false + tile.handleClick(null) testableLooper.processAllMessages() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt index 37107135c6d8..036d3c862ae0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt @@ -16,22 +16,25 @@ package com.android.systemui.qs.tiles.dialog.bluetooth -import android.content.pm.UserInfo +import android.bluetooth.BluetoothAdapter import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.util.settings.FakeSettings -import com.google.common.truth.Truth +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import kotlin.test.Test import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule @@ -41,8 +44,17 @@ class BluetoothAutoOnInteractorTest : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) - private var secureSettings: FakeSettings = FakeSettings() - private val userRepository: FakeUserRepository = FakeUserRepository() + private val bluetoothAdapter = + mock<BluetoothAdapter> { + var autoOn = false + whenever(isAutoOnEnabled).thenAnswer { autoOn } + + whenever(setAutoOnEnabled(anyBoolean())).thenAnswer { invocation -> + autoOn = invocation.getArgument(0) as Boolean + autoOn + } + } + @Mock private lateinit var localBluetoothManager: LocalBluetoothManager private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor @Before @@ -50,49 +62,35 @@ class BluetoothAutoOnInteractorTest : SysuiTestCase() { bluetoothAutoOnInteractor = BluetoothAutoOnInteractor( BluetoothAutoOnRepository( - secureSettings, - userRepository, + localBluetoothManager, + bluetoothAdapter, testScope.backgroundScope, - testDispatcher + testDispatcher, ) ) } @Test - fun testSet_bluetoothAutoOnUnset_doNothing() { + fun testSetEnabled_bluetoothAutoOnUnsupported_doNothing() { testScope.runTest { - bluetoothAutoOnInteractor.setEnabled(true) - - val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled) + whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(false) + bluetoothAutoOnInteractor.setEnabled(true) runCurrent() - Truth.assertThat(actualValue).isEqualTo(false) + assertFalse(bluetoothAdapter.isAutoOnEnabled) } } @Test - fun testSet_bluetoothAutoOnSet_setNewValue() { + fun testSetEnabled_bluetoothAutoOnSupported_setNewValue() { testScope.runTest { - userRepository.setUserInfos(listOf(SYSTEM_USER)) - secureSettings.putIntForUser( - BluetoothAutoOnRepository.SETTING_NAME, - BluetoothAutoOnInteractor.DISABLED, - SYSTEM_USER_ID - ) - bluetoothAutoOnInteractor.setEnabled(true) - - val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled) + whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(true) + bluetoothAutoOnInteractor.setEnabled(true) runCurrent() - Truth.assertThat(actualValue).isEqualTo(true) + assertTrue(bluetoothAdapter.isAutoOnEnabled) } } - - companion object { - private const val SYSTEM_USER_ID = 0 - private val SYSTEM_USER = - UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt index cd1452a6bf84..31192841ec77 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt @@ -16,18 +16,14 @@ package com.android.systemui.qs.tiles.dialog.bluetooth -import android.content.pm.UserInfo -import android.os.UserHandle +import android.bluetooth.BluetoothAdapter import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.BluetoothEventManager +import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.DISABLED -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.ENABLED -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.SETTING_NAME -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.UNSET -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -37,6 +33,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule @@ -46,83 +43,57 @@ class BluetoothAutoOnRepositoryTest : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) - private var secureSettings: FakeSettings = FakeSettings() - private val userRepository: FakeUserRepository = FakeUserRepository() + @Mock private lateinit var bluetoothAdapter: BluetoothAdapter + @Mock private lateinit var localBluetoothManager: LocalBluetoothManager + @Mock private lateinit var eventManager: BluetoothEventManager private lateinit var bluetoothAutoOnRepository: BluetoothAutoOnRepository @Before fun setUp() { + whenever(localBluetoothManager.eventManager).thenReturn(eventManager) bluetoothAutoOnRepository = BluetoothAutoOnRepository( - secureSettings, - userRepository, + localBluetoothManager, + bluetoothAdapter, testScope.backgroundScope, - testDispatcher + testDispatcher, ) - - userRepository.setUserInfos(listOf(SECONDARY_USER, SYSTEM_USER)) } @Test - fun testGetValue_valueUnset() { + fun testIsAutoOn_returnFalse() { testScope.runTest { - userRepository.setSelectedUserInfo(SYSTEM_USER) + whenever(bluetoothAdapter.isAutoOnEnabled).thenReturn(false) val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn) runCurrent() - assertThat(actualValue).isEqualTo(UNSET) - assertThat(bluetoothAutoOnRepository.isValuePresent()).isFalse() + assertThat(actualValue).isEqualTo(false) } } @Test - fun testGetValue_valueFalse() { + fun testIsAutoOn_returnTrue() { testScope.runTest { - userRepository.setSelectedUserInfo(SYSTEM_USER) + whenever(bluetoothAdapter.isAutoOnEnabled).thenReturn(true) val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn) - secureSettings.putIntForUser(SETTING_NAME, DISABLED, UserHandle.USER_SYSTEM) runCurrent() - assertThat(actualValue).isEqualTo(DISABLED) + assertThat(actualValue).isEqualTo(true) } } @Test - fun testGetValue_valueTrue() { + fun testIsAutoOnSupported_returnTrue() { testScope.runTest { - userRepository.setSelectedUserInfo(SYSTEM_USER) - val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn) + whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(true) + val actualValue = bluetoothAutoOnRepository.isAutoOnSupported() - secureSettings.putIntForUser(SETTING_NAME, ENABLED, UserHandle.USER_SYSTEM) runCurrent() - assertThat(actualValue).isEqualTo(ENABLED) + assertThat(actualValue).isEqualTo(true) } } - - @Test - fun testGetValue_valueTrue_secondaryUser_returnTrue() { - testScope.runTest { - userRepository.setSelectedUserInfo(SECONDARY_USER) - val actualValue by collectLastValue(bluetoothAutoOnRepository.isAutoOn) - - secureSettings.putIntForUser(SETTING_NAME, DISABLED, SYSTEM_USER_ID) - secureSettings.putIntForUser(SETTING_NAME, ENABLED, SECONDARY_USER_ID) - runCurrent() - - assertThat(actualValue).isEqualTo(ENABLED) - } - } - - companion object { - private const val SYSTEM_USER_ID = 0 - private const val SECONDARY_USER_ID = 1 - private val SYSTEM_USER = - UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0) - private val SECONDARY_USER = - UserInfo(/* id= */ SECONDARY_USER_ID, /* name= */ "secondary user", /* flags= */ 0) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt index 8ecb95334bc4..17b612714fe2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt @@ -109,7 +109,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { mBluetoothTileDialogDelegate = BluetoothTileDialogDelegate( - mContext, uiProperties, CONTENT_HEIGHT, ENABLED, @@ -119,14 +118,12 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { fakeSystemClock, uiEventLogger, logger, - sysuiDialogFactory, - LayoutInflater.from(mContext) + sysuiDialogFactory ) whenever( sysuiDialogFactory.create( - any(SystemUIDialog.Delegate::class.java), - any(Context::class.java) + any(SystemUIDialog.Delegate::class.java) ) ) .thenAnswer { @@ -216,7 +213,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) val viewHolder = BluetoothTileDialogDelegate( - mContext, uiProperties, CONTENT_HEIGHT, ENABLED, @@ -227,7 +223,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { uiEventLogger, logger, sysuiDialogFactory, - LayoutInflater.from(mContext) ) .Adapter(bluetoothTileDialogCallback) .DeviceItemViewHolder(view) @@ -273,7 +268,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { val cachedHeight = Int.MAX_VALUE val dialog = BluetoothTileDialogDelegate( - mContext, BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), cachedHeight, ENABLED, @@ -284,7 +278,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { uiEventLogger, logger, sysuiDialogFactory, - LayoutInflater.from(mContext) ) .createDialog() dialog.show() @@ -298,7 +291,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { testScope.runTest { val dialog = BluetoothTileDialogDelegate( - mContext, BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), MATCH_PARENT, ENABLED, @@ -309,7 +301,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { uiEventLogger, logger, sysuiDialogFactory, - LayoutInflater.from(mContext) ) .createDialog() dialog.show() @@ -323,7 +314,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { testScope.runTest { val dialog = BluetoothTileDialogDelegate( - mContext, BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), MATCH_PARENT, ENABLED, @@ -334,7 +324,6 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { uiEventLogger, logger, sysuiDialogFactory, - LayoutInflater.from(mContext) ) .createDialog() dialog.show() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt index 39e2413be40e..c8a2aa64ffa2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt @@ -16,7 +16,7 @@ package com.android.systemui.qs.tiles.dialog.bluetooth -import android.content.pm.UserInfo +import android.bluetooth.BluetoothAdapter import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -26,19 +26,18 @@ import android.widget.LinearLayout import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.flags.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.kotlin.getMutableStateFlow import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.nullable 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 kotlinx.coroutines.CoroutineDispatcher @@ -75,6 +74,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor + @Mock private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor + @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor @Mock private lateinit var activityStarter: ActivityStarter @@ -87,6 +88,10 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var bluetoothAdapter: BluetoothAdapter + + @Mock private lateinit var localBluetoothManager: LocalBluetoothManager + @Mock private lateinit var mBluetoothTileDialogDelegateDelegateFactory: BluetoothTileDialogDelegate.Factory @@ -100,8 +105,6 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { private lateinit var scheduler: TestCoroutineScheduler private lateinit var dispatcher: CoroutineDispatcher private lateinit var testScope: TestScope - private lateinit var secureSettings: FakeSettings - private lateinit var userRepository: FakeUserRepository @Before fun setUp() { @@ -109,14 +112,6 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { scheduler = TestCoroutineScheduler() dispatcher = UnconfinedTestDispatcher(scheduler) testScope = TestScope(dispatcher) - secureSettings = FakeSettings() - userRepository = FakeUserRepository() - userRepository.setUserInfos(listOf(SYSTEM_USER)) - secureSettings.putIntForUser( - BluetoothAutoOnRepository.SETTING_NAME, - BluetoothAutoOnInteractor.ENABLED, - SYSTEM_USER_ID - ) bluetoothTileDialogViewModel = BluetoothTileDialogViewModel( deviceItemInteractor, @@ -124,8 +119,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { // TODO(b/316822488): Create FakeBluetoothAutoOnInteractor. BluetoothAutoOnInteractor( BluetoothAutoOnRepository( - secureSettings, - userRepository, + localBluetoothManager, + bluetoothAdapter, testScope.backgroundScope, dispatcher ) @@ -148,7 +143,6 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { whenever( mBluetoothTileDialogDelegateDelegateFactory.create( any(), - any(), anyInt(), ArgumentMatchers.anyBoolean(), any(), @@ -157,6 +151,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { ) .thenReturn(bluetoothTileDialogDelegate) whenever(bluetoothTileDialogDelegate.createDialog()).thenReturn(sysuiDialog) + whenever(sysuiDialog.context).thenReturn(mContext) whenever(bluetoothTileDialogDelegate.bluetoothStateToggle) .thenReturn(getMutableStateFlow(false)) whenever(bluetoothTileDialogDelegate.deviceItemClick) @@ -169,7 +164,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Test fun testShowDialog_noAnimation() { testScope.runTest { - bluetoothTileDialogViewModel.showDialog(context, null) + bluetoothTileDialogViewModel.showDialog(null) verify(mDialogTransitionAnimator, never()).showFromView(any(), any(), any(), any()) } @@ -178,7 +173,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Test fun testShowDialog_animated() { testScope.runTest { - bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext)) + bluetoothTileDialogViewModel.showDialog(LinearLayout(mContext)) verify(mDialogTransitionAnimator).showFromView(any(), any(), nullable(), anyBoolean()) } @@ -188,7 +183,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testShowDialog_animated_callInBackgroundThread() { testScope.runTest { backgroundExecutor.execute { - bluetoothTileDialogViewModel.showDialog(mContext, LinearLayout(mContext)) + bluetoothTileDialogViewModel.showDialog(LinearLayout(mContext)) verify(mDialogTransitionAnimator) .showFromView(any(), any(), nullable(), anyBoolean()) @@ -199,7 +194,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Test fun testShowDialog_fetchDeviceItem() { testScope.runTest { - bluetoothTileDialogViewModel.showDialog(context, null) + bluetoothTileDialogViewModel.showDialog(null) verify(deviceItemInteractor).deviceItemUpdate } @@ -208,7 +203,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Test fun testShowDialog_withBluetoothStateValue() { testScope.runTest { - bluetoothTileDialogViewModel.showDialog(context, null) + bluetoothTileDialogViewModel.showDialog(null) verify(bluetoothStateInteractor).bluetoothStateUpdate } @@ -218,7 +213,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testStartSettingsActivity_activityLaunched_dialogDismissed() { testScope.runTest { whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) - bluetoothTileDialogViewModel.showDialog(context, null) + bluetoothTileDialogViewModel.showDialog(null) val clickedView = View(context) bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView) @@ -265,26 +260,22 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { } @Test - fun testIsAutoOnToggleFeatureAvailable_flagOn_settingValueSet_returnTrue() { + fun testIsAutoOnToggleFeatureAvailable_returnTrue() { testScope.runTest { + whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(true) + val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable() assertThat(actual).isTrue() } } @Test - fun testIsAutoOnToggleFeatureAvailable_flagOff_settingValueSet_returnFalse() { + fun testIsAutoOnToggleFeatureAvailable_returnFalse() { testScope.runTest { - mSetFlagsRule.disableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE) + whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(false) val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable() assertThat(actual).isFalse() } } - - companion object { - private const val SYSTEM_USER_ID = 0 - private val SYSTEM_USER = - UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt new file mode 100644 index 000000000000..4215b8c9a1a3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.work.ui + +import android.app.admin.DevicePolicyResources +import android.app.admin.DevicePolicyResourcesManager +import android.app.admin.devicePolicyManager +import android.graphics.drawable.TestStubDrawable +import android.service.quicksettings.Tile +import android.widget.Switch +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.qs.tiles.impl.work.qsWorkModeTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class WorkModeTileMapperTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val qsTileConfig = kosmos.qsWorkModeTileConfig + private val devicePolicyManager = kosmos.devicePolicyManager + private val testLabel = context.getString(R.string.quick_settings_work_mode_label) + private val devicePolicyResourceManager = mock<DevicePolicyResourcesManager>() + private lateinit var mapper: WorkModeTileMapper + + @Before + fun setup() { + whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourceManager) + whenever( + devicePolicyResourceManager.getString( + eq(DevicePolicyResources.Strings.SystemUi.QS_WORK_PROFILE_LABEL), + any() + ) + ) + .thenReturn(testLabel) + mapper = + WorkModeTileMapper( + context.orCreateTestableResources + .apply { + addOverride( + com.android.internal.R.drawable.stat_sys_managed_profile_status, + TestStubDrawable() + ) + } + .resources, + context.theme, + devicePolicyManager + ) + } + + @Test + fun mapsDisabledDataToInactiveState() { + val isEnabled = false + + val actualState: QSTileState = + mapper.map(qsTileConfig, WorkModeTileModel.HasActiveProfile(isEnabled)) + + val expectedState = createWorkModeTileState(QSTileState.ActivationState.INACTIVE) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun mapsEnabledDataToActiveState() { + val isEnabled = true + + val actualState: QSTileState = + mapper.map(qsTileConfig, WorkModeTileModel.HasActiveProfile(isEnabled)) + + val expectedState = createWorkModeTileState(QSTileState.ActivationState.ACTIVE) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun mapsNoActiveProfileDataToUnavailableState() { + val actualState: QSTileState = mapper.map(qsTileConfig, WorkModeTileModel.NoActiveProfile) + + val expectedState = createWorkModeTileState(QSTileState.ActivationState.UNAVAILABLE) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + private fun createWorkModeTileState( + activationState: QSTileState.ActivationState, + ): QSTileState { + val label = testLabel + return QSTileState( + icon = { + Icon.Loaded( + context.getDrawable( + com.android.internal.R.drawable.stat_sys_managed_profile_status + )!!, + null + ) + }, + label = label, + activationState = activationState, + secondaryLabel = + if (activationState == QSTileState.ActivationState.INACTIVE) { + context.getString(R.string.quick_settings_work_mode_paused_state) + } else if (activationState == QSTileState.ActivationState.UNAVAILABLE) { + context.resources + .getStringArray(R.array.tile_states_work)[Tile.STATE_UNAVAILABLE] + } else { + "" + }, + supportedActions = + if (activationState == QSTileState.ActivationState.UNAVAILABLE) { + setOf() + } else { + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + }, + contentDescription = label, + stateDescription = null, + sideViewIcon = QSTileState.SideViewIcon.None, + enabledState = QSTileState.EnabledState.ENABLED, + expandedAccessibilityClassName = Switch::class.qualifiedName + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt index 10d6ebf11be7..1313227c7f3d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt @@ -21,7 +21,7 @@ import android.content.Context import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.os.PowerManager -import android.os.Process; +import android.os.Process import android.os.UserHandle import android.testing.AndroidTestingRunner import android.testing.TestableContext @@ -34,8 +34,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager @@ -96,7 +94,6 @@ class OverviewProxyServiceTest : SysuiTestCase() { private val displayTracker = FakeDisplayTracker(mContext) private val fakeSystemClock = FakeSystemClock() private val sysUiState = SysUiState(displayTracker, kosmos.sceneContainerPlugin) - private val featureFlags = FakeFeatureFlags() private val wakefulnessLifecycle = WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager) @@ -121,8 +118,7 @@ class OverviewProxyServiceTest : SysuiTestCase() { @Mock private lateinit var unfoldTransitionProgressForwarder: Optional<UnfoldTransitionProgressForwarder> - @Mock - private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Before fun setUp() { @@ -205,16 +201,14 @@ class OverviewProxyServiceTest : SysuiTestCase() { @Test fun connectToOverviewService_primaryUser_expectBindService() { - val mockitoSession = ExtendedMockito.mockitoSession() - .spyStatic(Process::class.java) - .startMocking() + val mockitoSession = + ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking() try { `when`(Process.myUserHandle()).thenReturn(UserHandle.SYSTEM) val spyContext = spy(context) val ops = createOverviewProxyService(spyContext) ops.startConnectionToCurrentUser() - verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), - anyInt(), any()) + verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), anyInt(), any()) } finally { mockitoSession.finishMocking() } @@ -222,22 +216,20 @@ class OverviewProxyServiceTest : SysuiTestCase() { @Test fun connectToOverviewService_nonPrimaryUser_expectNoBindService() { - val mockitoSession = ExtendedMockito.mockitoSession() - .spyStatic(Process::class.java) - .startMocking() + val mockitoSession = + ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking() try { `when`(Process.myUserHandle()).thenReturn(UserHandle.of(12345)) val spyContext = spy(context) val ops = createOverviewProxyService(spyContext) ops.startConnectionToCurrentUser() - verify(spyContext, times(0)).bindServiceAsUser(any(), any(), - anyInt(), any()) + verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any()) } finally { mockitoSession.finishMocking() } } - private fun createOverviewProxyService(ctx: Context) : OverviewProxyService { + private fun createOverviewProxyService(ctx: Context): OverviewProxyService { return OverviewProxyService( ctx, executor, @@ -257,7 +249,6 @@ class OverviewProxyServiceTest : SysuiTestCase() { sysuiUnlockAnimationController, inWindowLauncherUnlockAnimationManager, assistUtils, - featureFlags, FakeSceneContainerFlags(), dumpManager, unfoldTransitionProgressForwarder, diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt index 2e8160baa257..1cfca68cd452 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt @@ -222,4 +222,9 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { ) verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>()) } + + @Test + fun startButton_isDisabled_beforeIssueTypeIsSelected() { + assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled).isFalse() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 43fcdf3eeedd..c25b910557a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -62,7 +62,6 @@ import android.view.accessibility.AccessibilityManager; import androidx.constraintlayout.widget.ConstraintSet; -import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.testing.UiEventLoggerFake; @@ -299,7 +298,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected RecordingController mRecordingController; @Mock protected LockscreenGestureLogger mLockscreenGestureLogger; @Mock protected DumpManager mDumpManager; - @Mock protected InteractionJankMonitor mInteractionJankMonitor; @Mock protected NotificationsQSContainerController mNotificationsQSContainerController; @Mock protected QsFrameTranslateController mQsFrameTranslateController; @Mock protected StatusBarWindowStateController mStatusBarWindowStateController; @@ -441,7 +439,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { SystemClock systemClock = new FakeSystemClock(); mStatusBarStateController = new StatusBarStateControllerImpl( mUiEventLogger, - mInteractionJankMonitor, + mKosmos.getInteractionJankMonitor(), mJavaAdapter, () -> mShadeInteractor, () -> mKosmos.getDeviceUnlockedInteractor(), @@ -459,7 +457,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mDozeParameters, mScreenOffAnimationController, mKeyguardLogger, - mInteractionJankMonitor, + mKosmos.getInteractionJankMonitor(), mKeyguardInteractor, mDumpManager, mPowerInteractor)); @@ -611,7 +609,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mock(HeadsUpManager.class), new StatusBarStateControllerImpl( new UiEventLoggerFake(), - mInteractionJankMonitor, + mKosmos.getInteractionJankMonitor(), mJavaAdapter, () -> mShadeInteractor, () -> mKosmos.getDeviceUnlockedInteractor(), @@ -651,10 +649,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { .thenReturn(mKeyguardBottomArea); when(mNotificationRemoteInputManager.isRemoteInputActive()) .thenReturn(false); - when(mInteractionJankMonitor.begin(any(), anyInt())) - .thenReturn(true); - when(mInteractionJankMonitor.end(anyInt())) - .thenReturn(true); doAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; @@ -820,7 +814,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mAccessibilityManager, mLockscreenGestureLogger, mMetricsLogger, - mInteractionJankMonitor, + mKosmos.getInteractionJankMonitor(), mShadeLog, mDumpManager, mDeviceEntryFaceAuthInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt index d35c7dd2d027..a92cf8c96339 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt @@ -21,7 +21,7 @@ import com.android.systemui.plugins.qs.QS import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.res.R -import com.android.systemui.shade.ShadeLockscreenInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index dcd000aaa011..648358234771 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -23,7 +23,6 @@ import android.platform.test.annotations.DisableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest -import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase @@ -39,6 +38,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsIntera import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.FakeFeatureFlagsClassic +import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.data.repository.FakeCommandQueue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository @@ -49,6 +49,7 @@ import com.android.systemui.keyguard.domain.interactor.fromGoneTransitionInterac import com.android.systemui.keyguard.domain.interactor.fromLockscreenTransitionInteractor import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.FakePowerRepository @@ -80,9 +81,7 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyFloat -import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito import org.mockito.Mockito.mock @@ -101,7 +100,6 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor private lateinit var fromPrimaryBouncerTransitionInteractor: FromPrimaryBouncerTransitionInteractor - private val interactionJankMonitor = mock<InteractionJankMonitor>() private val mockDarkAnimator = mock<ObjectAnimator>() private val deviceEntryUdfpsInteractor = mock<DeviceEntryUdfpsInteractor>() private val largeScreenHeaderHelper = mock<LargeScreenHeaderHelper>() @@ -112,15 +110,13 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true) - whenever(interactionJankMonitor.end(anyInt())).thenReturn(true) uiEventLogger = UiEventLoggerFake() underTest = object : StatusBarStateControllerImpl( uiEventLogger, - interactionJankMonitor, + kosmos.interactionJankMonitor, JavaAdapter(testScope.backgroundScope), { shadeInteractor }, { kosmos.deviceUnlockedInteractor }, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index 419b0fd2f89b..118d27a68c8c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -251,7 +251,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any()); - assertFalse(mParamsCaptor.getValue().isLowPriority()); + assertFalse(mParamsCaptor.getValue().isMinimized()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); // WHEN notification moves to a min priority section @@ -260,7 +260,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { // THEN we rebind it verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any()); - assertTrue(mParamsCaptor.getValue().isLowPriority()); + assertTrue(mParamsCaptor.getValue().isMinimized()); // THEN we do not filter it because it's not the first inflation. assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0)); @@ -273,7 +273,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any()); - assertTrue(mParamsCaptor.getValue().isLowPriority()); + assertTrue(mParamsCaptor.getValue().isMinimized()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); // WHEN notification is moved under a parent @@ -282,7 +282,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { // THEN we rebind it as not-minimized verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any()); - assertFalse(mParamsCaptor.getValue().isLowPriority()); + assertFalse(mParamsCaptor.getValue().isMinimized()); // THEN we do not filter it because it's not the first inflation. assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index b114e13bb25c..0e89d8072a2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -741,7 +741,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { when(mockViewWrapper.getIcon()).thenReturn(mockIcon); NotificationViewWrapper mockLowPriorityViewWrapper = mock(NotificationViewWrapper.class); - when(mockContainer.getLowPriorityViewWrapper()).thenReturn(mockLowPriorityViewWrapper); + when(mockContainer.getMinimizedGroupHeaderWrapper()).thenReturn(mockLowPriorityViewWrapper); CachingIconView mockLowPriorityIcon = mock(CachingIconView.class); when(mockLowPriorityViewWrapper.getIcon()).thenReturn(mockLowPriorityIcon); @@ -845,7 +845,6 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - @EnableFlags(com.android.systemui.Flags.FLAG_NOTIFICATION_ROW_USER_CONTEXT) public void imageResolver_differentNotificationUser_createsUserContext() throws Exception { UserHandle user = new UserHandle(33); Context userContext = new SysuiTestableContext(mContext); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index a0d10759ba56..8c225113677b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -231,6 +231,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { NotificationContentInflater.applyRemoteView( AsyncTask.SERIAL_EXECUTOR, false /* inflateSynchronously */, + /* isMinimized= */ false, result, FLAG_CONTENT_VIEW_EXPANDED, 0, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java index 65491937c285..fe0d9d06c8f4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java @@ -23,6 +23,7 @@ import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE; +import static com.android.systemui.concurrency.FakeExecutorKosmosKt.getFakeExecutor; import static com.android.systemui.statusbar.NotificationEntryHelper.modifyRanking; import static junit.framework.Assert.assertNotNull; @@ -124,12 +125,11 @@ public class NotificationGutsManagerTest extends SysuiTestCase { private NotificationChannel mTestNotificationChannel = new NotificationChannel( TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); - private KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); - private TestScope mTestScope = mKosmos.getTestScope(); - private JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope()); - private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); - private TestableLooper mTestableLooper; - private Handler mHandler; + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + private final TestScope mTestScope = mKosmos.getTestScope(); + private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope()); + private final FakeExecutor mExecutor = mKosmos.getFakeExecutor(); + private final Handler mHandler = mKosmos.getFakeExecutorHandler(); private NotificationTestHelper mHelper; private NotificationGutsManager mGutsManager; @@ -171,10 +171,8 @@ public class NotificationGutsManagerTest extends SysuiTestCase { @Before public void setUp() { - mTestableLooper = TestableLooper.get(this); allowTestableLooperAsMainThread(); - mHandler = Handler.createAsync(mTestableLooper.getLooper()); - mHelper = new NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)); + mHelper = new NotificationTestHelper(mContext, mDependency); when(mAccessibilityManager.isTouchExplorationEnabled()).thenReturn(false); mWindowRootViewVisibilityInteractor = new WindowRootViewVisibilityInteractor( @@ -248,7 +246,7 @@ public class NotificationGutsManagerTest extends SysuiTestCase { assertTrue(mGutsManager.openGutsInternal(row, 0, 0, menuItem)); assertEquals(View.INVISIBLE, guts.getVisibility()); - mTestableLooper.processAllMessages(); + mExecutor.runAllReady(); verify(guts).openControls( anyInt(), anyInt(), @@ -261,7 +259,7 @@ public class NotificationGutsManagerTest extends SysuiTestCase { verify(guts).closeControls(anyBoolean(), anyBoolean(), anyInt(), anyInt(), anyBoolean()); verify(row, times(1)).setGutsView(any()); - mTestableLooper.processAllMessages(); + mExecutor.runAllReady(); verify(mHeadsUpManager).setGutsShown(realRow.getEntry(), false); } @@ -352,7 +350,7 @@ public class NotificationGutsManagerTest extends SysuiTestCase { when(entry.getGuts()).thenReturn(guts); assertTrue(mGutsManager.openGutsInternal(row, 0, 0, menuItem)); - mTestableLooper.processAllMessages(); + mExecutor.runAllReady(); verify(guts).openControls( anyInt(), anyInt(), @@ -365,7 +363,7 @@ public class NotificationGutsManagerTest extends SysuiTestCase { row.onDensityOrFontScaleChanged(); mGutsManager.onDensityOrFontScaleChanged(entry); - mTestableLooper.processAllMessages(); + mExecutor.runAllReady(); mGutsManager.closeAndSaveGuts(false, false, false, 0, 0, false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt index 012ff2e31562..65a960b5ff6c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt @@ -27,12 +27,11 @@ import android.content.pm.ShortcutManager import android.content.pm.launcherApps import android.graphics.Color import android.os.Binder -import android.os.Handler +import android.os.fakeExecutorHandler import android.os.userManager import android.provider.Settings import android.service.notification.NotificationListenerService.Ranking import android.testing.AndroidTestingRunner -import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper import android.util.ArraySet import android.view.View @@ -45,6 +44,7 @@ import com.android.internal.logging.metricsLogger import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.statusbar.statusBarService import com.android.systemui.SysuiTestCase +import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.kosmos.testScope import com.android.systemui.people.widget.PeopleSpaceWidgetManager @@ -71,9 +71,7 @@ import com.android.systemui.statusbar.notificationLockscreenUserManager import com.android.systemui.statusbar.policy.deviceProvisionedController import com.android.systemui.statusbar.policy.headsUpManager import com.android.systemui.testKosmos -import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.kotlin.JavaAdapter -import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.wmshell.BubblesManager import java.util.Optional import junit.framework.Assert @@ -106,9 +104,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val javaAdapter = JavaAdapter(testScope.backgroundScope) - private val executor = FakeExecutor(FakeSystemClock()) - private lateinit var testableLooper: TestableLooper - private lateinit var handler: Handler + private val executor = kosmos.fakeExecutor + private val handler = kosmos.fakeExecutorHandler private lateinit var helper: NotificationTestHelper private lateinit var gutsManager: NotificationGutsManager private lateinit var windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor @@ -148,10 +145,8 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) val sceneContainerFlags = kosmos.fakeSceneContainerFlags sceneContainerFlags.enabled = true - testableLooper = TestableLooper.get(this) allowTestableLooperAsMainThread() - handler = Handler.createAsync(testableLooper.getLooper()) - helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + helper = NotificationTestHelper(mContext, mDependency) Mockito.`when`(accessibilityManager.isTouchExplorationEnabled).thenReturn(false) windowRootViewVisibilityInteractor = WindowRootViewVisibilityInteractor( @@ -227,7 +222,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { Mockito.`when`(row.guts).thenReturn(guts) Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem)) assertEquals(View.INVISIBLE.toLong(), guts.visibility.toLong()) - testableLooper.processAllMessages() + executor.runAllReady() verify(guts) .openControls( ArgumentMatchers.anyInt(), @@ -247,7 +242,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { ArgumentMatchers.anyBoolean() ) verify(row, Mockito.times(1)).setGutsView(ArgumentMatchers.any()) - testableLooper.processAllMessages() + executor.runAllReady() verify(headsUpManager).setGutsShown(realRow.entry, false) } @@ -343,7 +338,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { Mockito.`when`(entry.row).thenReturn(row) Mockito.`when`(entry.getGuts()).thenReturn(guts) Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem)) - testableLooper.processAllMessages() + executor.runAllReady() verify(guts) .openControls( ArgumentMatchers.anyInt(), @@ -356,7 +351,7 @@ class NotificationGutsManagerWithScenesTest : SysuiTestCase() { verify(row).setGutsView(ArgumentMatchers.any()) row.onDensityOrFontScaleChanged() gutsManager.onDensityOrFontScaleChanged(entry) - testableLooper.processAllMessages() + executor.runAllReady() gutsManager.closeAndSaveGuts(false, false, false, 0, 0, false) verify(guts) .closeControls( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index 09a3eb480a49..954335efd33a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -615,10 +615,8 @@ public class NotificationTestHelper { LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); - if (com.android.systemui.Flags.notificationRowUserContext()) { - inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock, - mRowInflaterTaskLogger)); - } + inflater.setFactory2(new RowInflaterTask.RowAsyncLayoutInflater(entry, mSystemClock, + mRowInflaterTaskLogger)); mRow = (ExpandableNotificationRow) inflater.inflate( R.layout.status_bar_notification_row, null /* root */, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java index 76470dbe6d21..1534c84fd99a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java @@ -197,7 +197,7 @@ public class RowContentBindStageTest extends SysuiTestCase { params.clearDirtyContentViews(); // WHEN low priority is set and stage executed. - params.setUseLowPriority(true); + params.setUseMinimized(true); mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { }); // THEN binder is called with use low priority and contracted/expanded are called to bind. @@ -210,7 +210,7 @@ public class RowContentBindStageTest extends SysuiTestCase { anyBoolean(), any()); BindParams usedParams = bindParamsCaptor.getValue(); - assertTrue(usedParams.isLowPriority); + assertTrue(usedParams.isMinimized); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java index 8f88501a38f7..a15b4cd37184 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java @@ -25,8 +25,6 @@ import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.Icon; import android.os.Bundle; import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.testing.TestableLooper.RunWithLooper; import android.view.LayoutInflater; import android.view.View; @@ -44,7 +42,6 @@ import org.junit.runner.RunWith; @RunWith(AndroidTestingRunner.class) @SmallTest -@RunWithLooper public class NotificationBigPictureTemplateViewWrapperTest extends SysuiTestCase { private View mView; @@ -53,11 +50,7 @@ public class NotificationBigPictureTemplateViewWrapperTest extends SysuiTestCase @Before public void setup() throws Exception { - allowTestableLooperAsMainThread(); - NotificationTestHelper helper = new NotificationTestHelper( - mContext, - mDependency, - TestableLooper.get(this)); + NotificationTestHelper helper = new NotificationTestHelper(mContext, mDependency); mView = LayoutInflater.from(mContext).inflate( com.android.internal.R.layout.notification_template_material_big_picture, null); mRow = helper.createRow(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt index 3fa68bb69da2..fe2971c46c32 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt @@ -18,8 +18,6 @@ package com.android.systemui.statusbar.notification.row.wrapper import android.graphics.drawable.AnimatedImageDrawable import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.testing.TestableLooper.RunWithLooper import android.view.View import androidx.test.filters.SmallTest import com.android.internal.R @@ -41,7 +39,6 @@ import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidTestingRunner::class) -@RunWithLooper class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() { private lateinit var mRow: ExpandableNotificationRow @@ -49,8 +46,7 @@ class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() { @Before fun setUp() { - allowTestableLooperAsMainThread() - helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + helper = NotificationTestHelper(mContext, mDependency) mRow = helper.createRow() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java index 45f7c5a6fdc0..2d72c7e0b714 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java @@ -17,8 +17,6 @@ package com.android.systemui.statusbar.notification.row.wrapper; import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.testing.TestableLooper.RunWithLooper; import android.view.View; import android.widget.RemoteViews; @@ -36,18 +34,13 @@ import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidTestingRunner.class) -@RunWithLooper public class NotificationCustomViewWrapperTest extends SysuiTestCase { private ExpandableNotificationRow mRow; @Before public void setUp() throws Exception { - allowTestableLooperAsMainThread(); - NotificationTestHelper helper = new NotificationTestHelper( - mContext, - mDependency, - TestableLooper.get(this)); + NotificationTestHelper helper = new NotificationTestHelper(mContext, mDependency); mRow = helper.createRow(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt index c0444b563a2c..f26c18b1d197 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt @@ -18,8 +18,6 @@ package com.android.systemui.statusbar.notification.row.wrapper import android.graphics.drawable.AnimatedImageDrawable import android.testing.AndroidTestingRunner -import android.testing.TestableLooper -import android.testing.TestableLooper.RunWithLooper import android.view.View import androidx.test.filters.SmallTest import com.android.internal.widget.MessagingGroup @@ -39,7 +37,6 @@ import org.mockito.Mockito.`when` as whenever @SmallTest @RunWith(AndroidTestingRunner::class) -@RunWithLooper class NotificationMessagingTemplateViewWrapperTest : SysuiTestCase() { private lateinit var mRow: ExpandableNotificationRow @@ -47,8 +44,7 @@ class NotificationMessagingTemplateViewWrapperTest : SysuiTestCase() { @Before fun setUp() { - allowTestableLooperAsMainThread() - helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + helper = NotificationTestHelper(mContext, mDependency) mRow = helper.createRow() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt index f7632aa37d4b..54eed26adaf3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapperTest.kt @@ -69,7 +69,7 @@ class NotificationTemplateViewWrapperTest : SysuiTestCase() { TestUiOffloadThread(looper.looper) ) - helper = NotificationTestHelper(mContext, mDependency, looper) + helper = NotificationTestHelper(mContext, mDependency) row = helper.createRow() // Some code in the view iterates through parents so we need some extra containers around // it. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java index 93a9e597ca90..e3a77d32b90f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java @@ -39,7 +39,6 @@ import org.junit.runner.RunWith; @RunWith(AndroidTestingRunner.class) @SmallTest -@RunWithLooper public class NotificationViewWrapperTest extends SysuiTestCase { private View mView; @@ -48,13 +47,9 @@ public class NotificationViewWrapperTest extends SysuiTestCase { @Before public void setup() throws Exception { - allowTestableLooperAsMainThread(); mView = mock(View.class); when(mView.getContext()).thenReturn(mContext); - NotificationTestHelper helper = new NotificationTestHelper( - mContext, - mDependency, - TestableLooper.get(this)); + NotificationTestHelper helper = new NotificationTestHelper(mContext, mDependency); mRow = helper.createRow(); mNotificationViewWrapper = new TestableNotificationViewWrapper(mContext, mView, mRow); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 1f38a73020b2..3b16f1416935 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -67,7 +67,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test public void testGetMaxAllowedVisibleChildren_lowPriority() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(), NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED); } @@ -81,7 +81,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test public void testGetMaxAllowedVisibleChildren_lowPriority_expandedChildren() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); mChildrenContainer.setChildrenExpanded(true); Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(), NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED); @@ -89,7 +89,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test public void testGetMaxAllowedVisibleChildren_lowPriority_userLocked() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); mChildrenContainer.setUserLocked(true); Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(), NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED); @@ -118,7 +118,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test public void testShowingAsLowPriority_lowPriority() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); Assert.assertTrue(mChildrenContainer.showingAsLowPriority()); } @@ -129,7 +129,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test public void testShowingAsLowPriority_lowPriority_expanded() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); mGroup.setExpandable(true); mGroup.setUserExpanded(true, false); Assert.assertFalse(mChildrenContainer.showingAsLowPriority()); @@ -140,7 +140,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { mGroup.setUserLocked(true); mGroup.setExpandable(true); mGroup.setUserExpanded(true); - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(), NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); } @@ -148,14 +148,14 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test @DisableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME) public void testLowPriorityHeaderCleared() { - mGroup.setIsLowPriority(true); + mGroup.setIsMinimized(true); NotificationHeaderView lowPriorityHeaderView = - mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader(); + mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader(); Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility()); Assert.assertSame(mChildrenContainer, lowPriorityHeaderView.getParent()); - mGroup.setIsLowPriority(false); + mGroup.setIsMinimized(false); assertNull(lowPriorityHeaderView.getParent()); - assertNull(mChildrenContainer.getLowPriorityViewWrapper()); + assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper()); } @Test @@ -169,7 +169,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test @EnableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME) public void testSetLowPriorityWithAsyncInflation_noHeaderReInflation() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); assertNull("We don't inflate header from the main thread with Async " + "Inflation enabled", mChildrenContainer.getCurrentHeaderView()); } @@ -179,21 +179,21 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { public void setLowPriorityBeforeLowPriorityHeaderSet() { //Given: the children container does not have a low-priority header, and is not low-priority - assertNull(mChildrenContainer.getLowPriorityViewWrapper()); - mGroup.setIsLowPriority(false); + assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper()); + mGroup.setIsMinimized(false); //When: set the children container to be low-priority and set the low-priority header - mGroup.setIsLowPriority(true); - mGroup.setLowPriorityGroupHeader(createHeaderView(/* lowPriorityHeader= */ true)); + mGroup.setIsMinimized(true); + mGroup.setMinimizedGroupHeader(createHeaderView(/* lowPriorityHeader= */ true)); //Then: the low-priority group header should be visible NotificationHeaderView lowPriorityHeaderView = - mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader(); + mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader(); Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility()); Assert.assertSame(mChildrenContainer, lowPriorityHeaderView.getParent()); //When: set the children container to be not low-priority and set the normal header - mGroup.setIsLowPriority(false); + mGroup.setIsMinimized(false); mGroup.setGroupHeader(createHeaderView(/* lowPriorityHeader= */ false)); //Then: the low-priority group header should not be visible , normal header should be @@ -211,9 +211,9 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { public void changeLowPriorityAfterHeaderSet() { //Given: the children container does not have headers, and is not low-priority - assertNull(mChildrenContainer.getLowPriorityViewWrapper()); + assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper()); assertNull(mChildrenContainer.getNotificationHeaderWrapper()); - mGroup.setIsLowPriority(false); + mGroup.setIsMinimized(false); //When: set the set the normal header mGroup.setGroupHeader(createHeaderView(/* lowPriorityHeader= */ false)); @@ -225,14 +225,14 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { Assert.assertSame(mChildrenContainer, headerView.getParent()); //When: set the set the row to be low priority, and set the low-priority header - mGroup.setIsLowPriority(true); - mGroup.setLowPriorityGroupHeader(createHeaderView(/* lowPriorityHeader= */ true)); + mGroup.setIsMinimized(true); + mGroup.setMinimizedGroupHeader(createHeaderView(/* lowPriorityHeader= */ true)); //Then: the header view should not be visible, the low-priority group header should be // visible Assert.assertEquals(View.INVISIBLE, headerView.getVisibility()); NotificationHeaderView lowPriorityHeaderView = - mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader(); + mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader(); Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility()); } @@ -263,7 +263,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test @DisableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME) public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_headerLowPriority() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper(); Assert.assertEquals(0f, header.getTopRoundness(), 0.001f); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index a4f88fbe1469..10d2191c0e07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -49,7 +49,6 @@ import android.view.ViewTreeObserver; import androidx.test.filters.SmallTest; -import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto; @@ -63,6 +62,7 @@ import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.TransitionStep; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.media.controls.ui.controller.KeyguardMediaController; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -130,6 +130,7 @@ import javax.inject.Provider; @RunWith(AndroidTestingRunner.class) public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { + protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); @Mock private NotificationGutsManager mNotificationGutsManager; @Mock private NotificationsController mNotificationsController; @@ -167,7 +168,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private SceneContainerFlags mSceneContainerFlags; @Mock private Provider<WindowRootView> mWindowRootView; @Mock private NotificationStackAppearanceInteractor mNotificationStackAppearanceInteractor; - @Mock private InteractionJankMonitor mJankMonitor; private final StackStateLogger mStackLogger = new StackStateLogger(logcatLogBuffer(), logcatLogBuffer()); private final NotificationStackScrollLogger mLogger = new NotificationStackScrollLogger( @@ -1030,7 +1030,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mSceneContainerFlags, mWindowRootView, mNotificationStackAppearanceInteractor, - mJankMonitor, + mKosmos.getInteractionJankMonitor(), mStackLogger, mLogger, mNotificationStackSizeCalculator, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index 0a18eb66c4df..c308a987455b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -35,9 +35,13 @@ import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.res.R import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository +import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor +import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository +import com.android.systemui.statusbar.notification.stack.data.repository.setNotifications import com.android.systemui.statusbar.policy.data.repository.fakeUserSetupRepository import com.android.systemui.statusbar.policy.data.repository.zenModeRepository import com.android.systemui.statusbar.policy.fakeConfigurationController @@ -70,6 +74,7 @@ class NotificationListViewModelTest : SysuiTestCase() { private val fakeRemoteInputRepository = kosmos.fakeRemoteInputRepository private val fakeShadeRepository = kosmos.fakeShadeRepository private val fakeUserSetupRepository = kosmos.fakeUserSetupRepository + private val headsUpRepository = kosmos.headsUpNotificationRepository private val zenModeRepository = kosmos.zenModeRepository val underTest = kosmos.notificationListViewModel @@ -125,35 +130,35 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldShowEmptyShadeView_trueWhenNoNotifs() = + fun testShouldIncludeEmptyShadeView_trueWhenNoNotifs() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) + val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) runCurrent() // THEN empty shade is visible - assertThat(shouldShow).isTrue() + assertThat(shouldInclude).isTrue() } @Test - fun testShouldShowEmptyShadeView_falseWhenNotifs() = + fun testShouldIncludeEmptyShadeView_falseWhenNotifs() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) + val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) runCurrent() // THEN empty shade is not visible - assertThat(shouldShow).isFalse() + assertThat(shouldInclude).isFalse() } @Test - fun testShouldShowEmptyShadeView_falseWhenQsExpandedDefault() = + fun testShouldIncludeEmptyShadeView_falseWhenQsExpandedDefault() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) + val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -162,13 +167,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN empty shade is not visible - assertThat(shouldShow).isFalse() + assertThat(shouldInclude).isFalse() } @Test - fun testShouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() = + fun testShouldIncludeEmptyShadeView_trueWhenQsExpandedInSplitShade() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) + val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -180,13 +185,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN empty shade is visible - assertThat(shouldShow).isTrue() + assertThat(shouldInclude).isTrue() } @Test - fun testShouldShowEmptyShadeView_trueWhenLockedShade() = + fun testShouldIncludeEmptyShadeView_trueWhenLockedShade() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) + val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -195,13 +200,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN empty shade is visible - assertThat(shouldShow).isTrue() + assertThat(shouldInclude).isTrue() } @Test - fun testShouldShowEmptyShadeView_falseWhenKeyguard() = + fun testShouldIncludeEmptyShadeView_falseWhenKeyguard() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) + val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -210,13 +215,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN empty shade is not visible - assertThat(shouldShow).isFalse() + assertThat(shouldInclude).isFalse() } @Test - fun testShouldShowEmptyShadeView_falseWhenStartingToSleep() = + fun testShouldIncludeEmptyShadeView_falseWhenStartingToSleep() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView) + val shouldInclude by collectLastValue(underTest.shouldShowEmptyShadeView) // WHEN has no notifs activeNotificationListRepository.setActiveNotifs(count = 0) @@ -227,7 +232,7 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN empty shade is not visible - assertThat(shouldShow).isFalse() + assertThat(shouldInclude).isFalse() } @Test @@ -277,9 +282,9 @@ class NotificationListViewModelTest : SysuiTestCase() { } @Test - fun testShouldShowFooterView_trueWhenShade() = + fun testShouldIncludeFooterView_trueWhenShade() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -289,13 +294,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN footer is visible - assertThat(shouldShow?.value).isTrue() + assertThat(shouldInclude?.value).isTrue() } @Test - fun testShouldShowFooterView_trueWhenLockedShade() = + fun testShouldIncludeFooterView_trueWhenLockedShade() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -305,13 +310,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN footer is visible - assertThat(shouldShow?.value).isTrue() + assertThat(shouldInclude?.value).isTrue() } @Test - fun testShouldShowFooterView_falseWhenKeyguard() = + fun testShouldIncludeFooterView_falseWhenKeyguard() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -320,13 +325,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN footer is not visible - assertThat(shouldShow?.value).isFalse() + assertThat(shouldInclude?.value).isFalse() } @Test - fun testShouldShowFooterView_falseWhenUserNotSetUp() = + fun testShouldIncludeFooterView_falseWhenUserNotSetUp() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -338,13 +343,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN footer is not visible - assertThat(shouldShow?.value).isFalse() + assertThat(shouldInclude?.value).isFalse() } @Test - fun testShouldShowFooterView_falseWhenStartingToSleep() = + fun testShouldIncludeFooterView_falseWhenStartingToSleep() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -356,13 +361,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN footer is not visible - assertThat(shouldShow?.value).isFalse() + assertThat(shouldInclude?.value).isFalse() } @Test - fun testShouldShowFooterView_falseWhenQsExpandedDefault() = + fun testShouldIncludeFooterView_falseWhenQsExpandedDefault() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -375,13 +380,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN footer is not visible - assertThat(shouldShow?.value).isFalse() + assertThat(shouldInclude?.value).isFalse() } @Test - fun testShouldShowFooterView_trueWhenQsExpandedSplitShade() = + fun testShouldIncludeFooterView_trueWhenQsExpandedSplitShade() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -396,13 +401,13 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN footer is visible - assertThat(shouldShow?.value).isTrue() + assertThat(shouldInclude?.value).isTrue() } @Test - fun testShouldShowFooterView_falseWhenRemoteInputActive() = + fun testShouldIncludeFooterView_falseWhenRemoteInputActive() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) @@ -414,54 +419,182 @@ class NotificationListViewModelTest : SysuiTestCase() { runCurrent() // THEN footer is not visible - assertThat(shouldShow?.value).isFalse() + assertThat(shouldInclude?.value).isFalse() } @Test - fun testShouldShowFooterView_falseWhenShadeIsClosed() = + fun testShouldIncludeFooterView_animatesWhenShade() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) - // AND shade is closed + // AND shade is open and fully expanded fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) - fakeShadeRepository.setLegacyShadeExpansion(0f) + fakeShadeRepository.setLegacyShadeExpansion(1f) runCurrent() - // THEN footer is not visible - assertThat(shouldShow?.value).isFalse() + // THEN footer visibility animates + assertThat(shouldInclude?.isAnimating).isTrue() } @Test - fun testShouldShowFooterView_animatesWhenShade() = + fun testShouldIncludeFooterView_notAnimatingOnKeyguard() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldInclude by collectLastValue(underTest.shouldIncludeFooterView) // WHEN has notifs activeNotificationListRepository.setActiveNotifs(count = 2) - // AND shade is open and fully expanded - fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + // AND we are on the keyguard + fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) fakeShadeRepository.setLegacyShadeExpansion(1f) runCurrent() - // THEN footer visibility animates - assertThat(shouldShow?.isAnimating).isTrue() + // THEN footer visibility does not animate + assertThat(shouldInclude?.isAnimating).isFalse() } @Test - fun testShouldShowFooterView_notAnimatingOnKeyguard() = + fun testShouldHideFooterView_trueWhenShadeIsClosed() = testScope.runTest { - val shouldShow by collectLastValue(underTest.shouldShowFooterView) + val shouldHide by collectLastValue(underTest.shouldHideFooterView) - // WHEN has notifs - activeNotificationListRepository.setActiveNotifs(count = 2) - // AND we are on the keyguard - fakeKeyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) + // WHEN shade is closed + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) + fakeShadeRepository.setLegacyShadeExpansion(0f) + runCurrent() + + // THEN footer is hidden + assertThat(shouldHide).isTrue() + } + + @Test + fun testShouldHideFooterView_falseWhenShadeIsOpen() = + testScope.runTest { + val shouldHide by collectLastValue(underTest.shouldHideFooterView) + + // WHEN shade is open + fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE) fakeShadeRepository.setLegacyShadeExpansion(1f) runCurrent() - // THEN footer visibility does not animate - assertThat(shouldShow?.isAnimating).isFalse() + // THEN footer is hidden + assertThat(shouldHide).isFalse() + } + + @Test + @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) + fun testPinnedHeadsUpRows_filtersForPinnedItems() = + testScope.runTest { + val pinnedHeadsUpRows by collectLastValue(underTest.pinnedHeadsUpRows) + + // WHEN there are no pinned rows + val rows = + arrayListOf( + fakeHeadsUpRowRepository(key = "0"), + fakeHeadsUpRowRepository(key = "1"), + fakeHeadsUpRowRepository(key = "2"), + ) + headsUpRepository.setNotifications( + rows, + ) + runCurrent() + + // THEN the list is empty + assertThat(pinnedHeadsUpRows).isEmpty() + + // WHEN a row gets pinned + rows[0].isPinned.value = true + runCurrent() + + // THEN it's added to the list + assertThat(pinnedHeadsUpRows).containsExactly(rows[0]) + + // WHEN more rows are pinned + rows[1].isPinned.value = true + runCurrent() + + // THEN they are all in the list + assertThat(pinnedHeadsUpRows).containsExactly(rows[0], rows[1]) + + // WHEN a row gets unpinned + rows[0].isPinned.value = false + runCurrent() + + // THEN it's removed from the list + assertThat(pinnedHeadsUpRows).containsExactly(rows[1]) + } + + @Test + @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) + fun testHasPinnedHeadsUpRows_true() = + testScope.runTest { + val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow) + + headsUpRepository.setNotifications( + fakeHeadsUpRowRepository(key = "0", isPinned = true), + fakeHeadsUpRowRepository(key = "1") + ) + runCurrent() + + assertThat(hasPinnedHeadsUpRow).isTrue() + } + + @Test + @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) + fun testHasPinnedHeadsUpRows_false() = + testScope.runTest { + val hasPinnedHeadsUpRow by collectLastValue(underTest.hasPinnedHeadsUpRow) + + headsUpRepository.setNotifications( + fakeHeadsUpRowRepository(key = "0"), + fakeHeadsUpRowRepository(key = "1"), + ) + runCurrent() + + assertThat(hasPinnedHeadsUpRow).isFalse() + } + + @Test + @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) + fun testTopHeadsUpRow_emptyList_null() = + testScope.runTest { + val topHeadsUpRow by collectLastValue(underTest.topHeadsUpRow) + + headsUpRepository.setNotifications(emptyList()) + runCurrent() + + assertThat(topHeadsUpRow).isNull() + } + + @Test + @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) + fun testHeadsUpAnimationsEnabled_true() = + testScope.runTest { + val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled) + + fakeShadeRepository.setQsExpansion(0.0f) + fakeKeyguardRepository.setKeyguardShowing(false) + runCurrent() + + assertThat(animationsEnabled).isTrue() + } + + @Test + @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME) + fun testHeadsUpAnimationsEnabled_keyguardShowing_false() = + testScope.runTest { + val animationsEnabled by collectLastValue(underTest.headsUpAnimationsEnabled) + + fakeShadeRepository.setQsExpansion(0.0f) + fakeKeyguardRepository.setKeyguardShowing(true) + runCurrent() + + assertThat(animationsEnabled).isFalse() + } + + private fun fakeHeadsUpRowRepository(key: String, isPinned: Boolean = false) = + FakeHeadsUpRowRepository(key = key, elementKey = Any()).apply { + this.isPinned.value = isPinned } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java index d5c40538586e..8e8dd4d91e8b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java @@ -188,7 +188,7 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { public void vibrateOnNavigationKeyDown_usesPerformHapticFeedback() { mSbcqCallbacks.vibrateOnNavigationKeyDown(); - verify(mShadeViewController).performHapticFeedback( + verify(mShadeController).performHapticFeedback( HapticFeedbackConstants.GESTURE_START ); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index b0b9bec4f721..054680df1582 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -94,7 +94,7 @@ import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.shade.ShadeExpansionStateManager; -import com.android.systemui.shade.ShadeLockscreenInteractor; +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt index de1891355f29..c8ff20b31aae 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor +import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.StatusBarStateControllerImpl @@ -68,6 +69,8 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { @Mock private lateinit var shadeViewController: ShadeViewController @Mock + private lateinit var shadeLockscreenInteractor: ShadeLockscreenInteractor + @Mock private lateinit var panelExpansionInteractor: PanelExpansionInteractor @Mock private lateinit var notifShadeWindowController: NotificationShadeWindowController @@ -100,6 +103,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { { notifShadeWindowController }, interactionJankMonitor, powerManager, + { shadeLockscreenInteractor }, { panelExpansionInteractor }, handler, ) @@ -133,7 +137,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { callbackCaptor.value.run() - verify(shadeViewController, times(1)).showAodUi() + verify(shadeLockscreenInteractor, times(1)).showAodUi() } @Test @@ -149,7 +153,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { callbackCaptor.value.run() - verify(shadeViewController, never()).showAodUi() + verify(shadeLockscreenInteractor, never()).showAodUi() } /** @@ -171,7 +175,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { verify(handler).postDelayed(callbackCaptor.capture(), anyLong()) callbackCaptor.value.run() - verify(shadeViewController, never()).showAodUi() + verify(shadeLockscreenInteractor, never()).showAodUi() } @Test @@ -186,7 +190,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { verify(handler).postDelayed(callbackCaptor.capture(), anyLong()) callbackCaptor.value.run() - verify(shadeViewController).showAodUi() + verify(shadeLockscreenInteractor).showAodUi() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt index 98556514f8ec..f761bcfe63d6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt @@ -868,6 +868,24 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test + fun networkName_usingEagerStrategy_retainsNameBetweenSubscribers() = + testScope.runTest { + // Use the [StateFlow.value] getter so we can prove that the collection happens + // even when there is no [Job] + + // Starts out default + assertThat(underTest.networkName.value).isEqualTo(DEFAULT_NAME_MODEL) + + val intent = spnIntent() + val captor = argumentCaptor<BroadcastReceiver>() + verify(context).registerReceiver(captor.capture(), any()) + captor.value!!.onReceive(context, intent) + + // The value is still there despite no active subscribers + assertThat(underTest.networkName.value).isEqualTo(intent.toNetworkNameModel(SEP)) + } + + @Test fun operatorAlphaShort_tracked() = testScope.runTest { var latest: String? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt index 933b5b519672..358709f48ea8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.policy import android.app.IActivityManager +import android.content.pm.PackageManager import android.media.projection.MediaProjectionManager import android.os.Handler import android.platform.test.annotations.DisableFlags @@ -44,6 +45,7 @@ class SensitiveNotificationProtectionControllerFlagDisabledTest : SysuiTestCase( @Mock private lateinit var handler: Handler @Mock private lateinit var activityManager: IActivityManager @Mock private lateinit var mediaProjectionManager: MediaProjectionManager + @Mock private lateinit var packageManager: PackageManager private lateinit var controller: SensitiveNotificationProtectionControllerImpl @Before @@ -56,6 +58,7 @@ class SensitiveNotificationProtectionControllerFlagDisabledTest : SysuiTestCase( FakeGlobalSettings(), mediaProjectionManager, activityManager, + packageManager, handler, FakeExecutor(FakeSystemClock()), logger diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt index 4b4e315f5533..7dfe6d01912f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt @@ -25,9 +25,14 @@ import android.app.Notification.VISIBILITY_PUBLIC import android.app.NotificationChannel import android.app.NotificationManager.IMPORTANCE_HIGH import android.app.NotificationManager.VISIBILITY_NO_OVERRIDE +import android.content.pm.PackageManager import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager +import android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION import android.platform.test.annotations.EnableFlags +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -48,9 +53,11 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.times @@ -64,10 +71,13 @@ import org.mockito.MockitoAnnotations @RunWithLooper @EnableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { + @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + private val logger = SensitiveNotificationProtectionControllerLogger(logcatLogBuffer()) @Mock private lateinit var activityManager: IActivityManager @Mock private lateinit var mediaProjectionManager: MediaProjectionManager + @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo @Mock private lateinit var listener1: Runnable @Mock private lateinit var listener2: Runnable @@ -87,6 +97,9 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { whenever(activityManager.bugreportWhitelistedPackages) .thenReturn(listOf(BUGREPORT_PACKAGE_NAME)) + whenever(packageManager.checkPermission(anyString(), anyString())) + .thenReturn(PackageManager.PERMISSION_DENIED) + executor = FakeExecutor(FakeSystemClock()) globalSettings = FakeGlobalSettings() controller = @@ -95,6 +108,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { globalSettings, mediaProjectionManager, activityManager, + packageManager, mockExecutorHandler(executor), executor, logger @@ -237,6 +251,36 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION) + fun isSensitiveStateActive_projectionActive_permissionExempt_flagDisabled_true() { + whenever( + packageManager.checkPermission( + android.Manifest.permission.RECORD_SENSITIVE_CONTENT, + mediaProjectionInfo.packageName + ) + ) + .thenReturn(PackageManager.PERMISSION_GRANTED) + mediaProjectionCallback.onStart(mediaProjectionInfo) + + assertTrue(controller.isSensitiveStateActive) + } + + @Test + @RequiresFlagsEnabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION) + fun isSensitiveStateActive_projectionActive_permissionExempt_false() { + whenever( + packageManager.checkPermission( + android.Manifest.permission.RECORD_SENSITIVE_CONTENT, + mediaProjectionInfo.packageName + ) + ) + .thenReturn(PackageManager.PERMISSION_GRANTED) + mediaProjectionCallback.onStart(mediaProjectionInfo) + + assertFalse(controller.isSensitiveStateActive) + } + + @Test fun isSensitiveStateActive_projectionActive_bugReportHandlerExempt_false() { whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME) mediaProjectionCallback.onStart(mediaProjectionInfo) @@ -309,6 +353,40 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION) + fun shouldProtectNotification_projectionActive_permissionExempt_flagDisabled_true() { + whenever( + packageManager.checkPermission( + android.Manifest.permission.RECORD_SENSITIVE_CONTENT, + mediaProjectionInfo.packageName + ) + ) + .thenReturn(PackageManager.PERMISSION_GRANTED) + mediaProjectionCallback.onStart(mediaProjectionInfo) + + val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) + + assertTrue(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + @RequiresFlagsEnabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION) + fun shouldProtectNotification_projectionActive_permissionExempt_false() { + whenever( + packageManager.checkPermission( + android.Manifest.permission.RECORD_SENSITIVE_CONTENT, + mediaProjectionInfo.packageName + ) + ) + .thenReturn(PackageManager.PERMISSION_GRANTED) + mediaProjectionCallback.onStart(mediaProjectionInfo) + + val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test fun shouldProtectNotification_projectionActive_bugReportHandlerExempt_false() { whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME) mediaProjectionCallback.onStart(mediaProjectionInfo) @@ -327,6 +405,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { assertFalse(controller.shouldProtectNotification(notificationEntry)) } + @Test fun shouldProtectNotification_projectionActive_publicNotification_false() { mediaProjectionCallback.onStart(mediaProjectionInfo) diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt index 7c36a85243a2..7a83cfe852d6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt @@ -19,7 +19,9 @@ package com.android.systemui.surfaceeffects.loadingeffect import android.graphics.Paint import android.graphics.RenderEffect import android.testing.AndroidTestingRunner +import android.testing.TestableLooper import androidx.test.filters.SmallTest +import com.android.systemui.animation.AnimatorTestRule import com.android.systemui.model.SysUiStateTest import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.EASE_IN @@ -31,18 +33,17 @@ import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.RenderEffectDrawCallback import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) class LoadingEffectTest : SysUiStateTest() { - private val fakeSystemClock = FakeSystemClock() - private val fakeExecutor = FakeExecutor(fakeSystemClock) + @get:Rule val animatorTestRule = AnimatorTestRule(this) @Test fun play_paintCallback_triggersDrawCallback() { @@ -61,14 +62,12 @@ class LoadingEffectTest : SysUiStateTest() { animationStateChangedCallback = null ) - fakeExecutor.execute { - assertThat(paintFromCallback).isNull() + assertThat(paintFromCallback).isNull() - loadingEffect.play() - fakeSystemClock.advanceTime(500L) + loadingEffect.play() + animatorTestRule.advanceTimeBy(500L) - assertThat(paintFromCallback).isNotNull() - } + assertThat(paintFromCallback).isNotNull() } @Test @@ -88,25 +87,22 @@ class LoadingEffectTest : SysUiStateTest() { animationStateChangedCallback = null ) - fakeExecutor.execute { - assertThat(renderEffectFromCallback).isNull() + assertThat(renderEffectFromCallback).isNull() - loadingEffect.play() - fakeSystemClock.advanceTime(500L) + loadingEffect.play() + animatorTestRule.advanceTimeBy(500L) - assertThat(renderEffectFromCallback).isNotNull() - } + assertThat(renderEffectFromCallback).isNotNull() } @Test fun play_animationStateChangesInOrder() { val config = TurbulenceNoiseAnimationConfig() - val expectedStates = arrayOf(NOT_PLAYING, EASE_IN, MAIN, EASE_OUT, NOT_PLAYING) - val actualStates = mutableListOf(NOT_PLAYING) + val states = mutableListOf(NOT_PLAYING) val stateChangedCallback = object : AnimationStateChangedCallback { override fun onStateChanged(oldState: AnimationState, newState: AnimationState) { - actualStates.add(newState) + states.add(newState) } } val drawCallback = @@ -121,16 +117,15 @@ class LoadingEffectTest : SysUiStateTest() { stateChangedCallback ) - val timeToAdvance = - config.easeInDuration + config.maxDuration + config.easeOutDuration + 100 + loadingEffect.play() - fakeExecutor.execute { - loadingEffect.play() + // Execute all the animators by advancing each duration with some buffer. + animatorTestRule.advanceTimeBy(config.easeInDuration.toLong()) + animatorTestRule.advanceTimeBy(config.maxDuration.toLong()) + animatorTestRule.advanceTimeBy(config.easeOutDuration.toLong()) + animatorTestRule.advanceTimeBy(500) - fakeSystemClock.advanceTime(timeToAdvance.toLong()) - - assertThat(actualStates).isEqualTo(expectedStates) - } + assertThat(states).containsExactly(NOT_PLAYING, EASE_IN, MAIN, EASE_OUT, NOT_PLAYING) } @Test @@ -157,17 +152,15 @@ class LoadingEffectTest : SysUiStateTest() { stateChangedCallback ) - fakeExecutor.execute { - assertThat(numPlay).isEqualTo(0) + assertThat(numPlay).isEqualTo(0) - loadingEffect.play() - loadingEffect.play() - loadingEffect.play() - loadingEffect.play() - loadingEffect.play() + loadingEffect.play() + loadingEffect.play() + loadingEffect.play() + loadingEffect.play() + loadingEffect.play() - assertThat(numPlay).isEqualTo(1) - } + assertThat(numPlay).isEqualTo(1) } @Test @@ -181,7 +174,7 @@ class LoadingEffectTest : SysUiStateTest() { val stateChangedCallback = object : AnimationStateChangedCallback { override fun onStateChanged(oldState: AnimationState, newState: AnimationState) { - if (oldState == MAIN && newState == NOT_PLAYING) { + if (oldState == EASE_OUT && newState == NOT_PLAYING) { isFinished = true } } @@ -194,18 +187,17 @@ class LoadingEffectTest : SysUiStateTest() { stateChangedCallback ) - fakeExecutor.execute { - assertThat(isFinished).isFalse() + assertThat(isFinished).isFalse() - loadingEffect.play() - fakeSystemClock.advanceTime(config.easeInDuration.toLong() + 500L) + loadingEffect.play() + animatorTestRule.advanceTimeBy(config.easeInDuration.toLong() + 500L) - assertThat(isFinished).isFalse() + assertThat(isFinished).isFalse() - loadingEffect.finish() + loadingEffect.finish() + animatorTestRule.advanceTimeBy(config.easeOutDuration.toLong() + 500L) - assertThat(isFinished).isTrue() - } + assertThat(isFinished).isTrue() } @Test @@ -232,13 +224,11 @@ class LoadingEffectTest : SysUiStateTest() { stateChangedCallback ) - fakeExecutor.execute { - assertThat(isFinished).isFalse() + assertThat(isFinished).isFalse() - loadingEffect.finish() + loadingEffect.finish() - assertThat(isFinished).isFalse() - } + assertThat(isFinished).isFalse() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt index 549280a809e2..e62ca645d772 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShaderTest.kt @@ -20,6 +20,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE_FRACTAL +import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE_SPARKLE import org.junit.Test import org.junit.runner.RunWith @@ -38,4 +39,9 @@ class TurbulenceNoiseShaderTest : SysuiTestCase() { fun compilesFractalNoise() { turbulenceNoiseShader = TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_FRACTAL) } + + @Test + fun compilesSparkleNoise() { + turbulenceNoiseShader = TurbulenceNoiseShader(baseType = SIMPLEX_NOISE_SPARKLE) + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt index 6ef74194fd85..ba07a849469d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt @@ -19,4 +19,5 @@ package com.android.systemui.biometrics.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.facePropertyRepository by Fixture { FakeFacePropertyRepository() } +val Kosmos.fakeFacePropertyRepository by Fixture { FakeFacePropertyRepository() } +val Kosmos.facePropertyRepository by Fixture { fakeFacePropertyRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt index 27803b22de29..c06554573bd7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.bouncer.domain.interactor -import android.content.applicationContext import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.data.repository.bouncerRepository import com.android.systemui.classifier.domain.interactor.falsingInteractor @@ -29,12 +28,10 @@ import com.android.systemui.power.domain.interactor.powerInteractor val Kosmos.bouncerInteractor by Fixture { BouncerInteractor( applicationScope = testScope.backgroundScope, - applicationContext = applicationContext, repository = bouncerRepository, authenticationInteractor = authenticationInteractor, deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, falsingInteractor = falsingInteractor, powerInteractor = powerInteractor, - simBouncerInteractor = simBouncerInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt index 8ed9f45bd1ba..02b79af15c05 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt @@ -38,7 +38,7 @@ val Kosmos.simBouncerInteractor by Fixture { telephonyManager = telephonyManager, resources = mainResources, keyguardUpdateMonitor = keyguardUpdateMonitor, - euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager, + euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager?, mobileConnectionsRepository = mobileConnectionsRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt new file mode 100644 index 000000000000..4b6441628500 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.bouncer.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor +import com.android.systemui.bouncer.shared.flag.composeBouncerFlags +import com.android.systemui.deviceentry.domain.interactor.biometricMessageInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel +import com.android.systemui.util.time.systemClock +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.bouncerMessageViewModel by + Kosmos.Fixture { + BouncerMessageViewModel( + applicationContext = applicationContext, + applicationScope = testScope.backgroundScope, + bouncerInteractor = bouncerInteractor, + simBouncerInteractor = simBouncerInteractor, + authenticationInteractor = authenticationInteractor, + selectedUser = userSwitcherViewModel.selectedUser, + clock = systemClock, + biometricMessageInteractor = biometricMessageInteractor, + faceAuthInteractor = deviceEntryFaceAuthInteractor, + deviceEntryInteractor = deviceEntryInteractor, + fingerprintInteractor = deviceEntryFingerprintAuthInteractor, + flags = composeBouncerFlags, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt index 6d97238ba48b..0f6c7cf13211 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.bouncer.ui.viewmodel import android.content.applicationContext @@ -30,7 +32,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.user.domain.interactor.selectedUserInteractor import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel import com.android.systemui.util.mockito.mock -import com.android.systemui.util.time.systemClock +import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.bouncerViewModel by Fixture { BouncerViewModel( @@ -47,7 +49,7 @@ val Kosmos.bouncerViewModel by Fixture { users = userSwitcherViewModel.users, userSwitcherMenu = userSwitcherViewModel.menu, actionButton = bouncerActionButtonInteractor.actionButton, - clock = systemClock, devicePolicyManager = mock(), + bouncerMessageViewModel = bouncerMessageViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/EmptyVibrator.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/EmptyVibrator.kt new file mode 100644 index 000000000000..875f6ed8d4a8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/EmptyVibrator.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.haptics + +import android.os.VibrationAttributes +import android.os.VibrationEffect +import android.os.Vibrator + +/** A simple empty vibrator required for the [FakeVibratorHelper] */ +class EmptyVibrator : Vibrator() { + override fun cancel() {} + + override fun cancel(usageFilter: Int) {} + + override fun hasAmplitudeControl(): Boolean = true + + override fun hasVibrator(): Boolean = true + + override fun vibrate( + uid: Int, + opPkg: String, + vibe: VibrationEffect, + reason: String, + attributes: VibrationAttributes, + ) {} +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/FakeVibratorHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/FakeVibratorHelper.kt new file mode 100644 index 000000000000..4c0b132210f1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/FakeVibratorHelper.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.haptics + +import android.annotation.SuppressLint +import android.media.AudioAttributes +import android.os.VibrationAttributes +import android.os.VibrationEffect +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.time.FakeSystemClock + +/** A fake [VibratorHelper] that only keeps track of the latest vibration effects delivered */ +@SuppressLint("VisibleForTests") +class FakeVibratorHelper : VibratorHelper(EmptyVibrator(), FakeExecutor(FakeSystemClock())) { + + /** A customizable map of primitive ids and their durations in ms */ + val primitiveDurations: HashMap<Int, Int> = ALL_PRIMITIVE_DURATIONS + + private val vibrationEffectHistory = ArrayList<VibrationEffect>() + + val totalVibrations: Int + get() = vibrationEffectHistory.size + + override fun vibrate(effect: VibrationEffect) { + vibrationEffectHistory.add(effect) + } + + override fun vibrate(effect: VibrationEffect, attributes: VibrationAttributes) = vibrate(effect) + + override fun vibrate(effect: VibrationEffect, attributes: AudioAttributes) = vibrate(effect) + + override fun vibrate( + uid: Int, + opPkg: String?, + vibe: VibrationEffect, + reason: String?, + attributes: VibrationAttributes, + ) = vibrate(vibe) + + override fun getPrimitiveDurations(vararg primitiveIds: Int): IntArray = + primitiveIds.map { primitiveDurations[it] ?: 0 }.toIntArray() + + fun hasVibratedWithEffects(vararg effects: VibrationEffect): Boolean = + vibrationEffectHistory.containsAll(effects.toList()) + + fun timesVibratedWithEffect(effect: VibrationEffect): Int = + vibrationEffectHistory.count { it == effect } + + companion object { + val ALL_PRIMITIVE_DURATIONS = + hashMapOf( + VibrationEffect.Composition.PRIMITIVE_NOOP to 0, + VibrationEffect.Composition.PRIMITIVE_CLICK to 12, + VibrationEffect.Composition.PRIMITIVE_THUD to 300, + VibrationEffect.Composition.PRIMITIVE_SPIN to 133, + VibrationEffect.Composition.PRIMITIVE_QUICK_RISE to 150, + VibrationEffect.Composition.PRIMITIVE_SLOW_RISE to 500, + VibrationEffect.Composition.PRIMITIVE_QUICK_FALL to 100, + VibrationEffect.Composition.PRIMITIVE_TICK to 5, + VibrationEffect.Composition.PRIMITIVE_LOW_TICK to 12, + ) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/VibratorHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/VibratorHelperKosmos.kt new file mode 100644 index 000000000000..434953fb2f43 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/haptics/VibratorHelperKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.haptics + +import com.android.systemui.kosmos.Kosmos + +var Kosmos.vibratorHelper by Kosmos.Fixture { FakeVibratorHelper() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt index 5b642ea645f5..eba5a11cecdb 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt @@ -45,15 +45,9 @@ class FakeKeyguardClockRepository @Inject constructor() : KeyguardClockRepositor private val _currentClock: MutableStateFlow<ClockController?> = MutableStateFlow(null) override val currentClock = _currentClock - private val _previewClockPair = - MutableStateFlow( - Pair( - Mockito.mock(ClockController::class.java), - Mockito.mock(ClockController::class.java) - ) - ) - override val previewClockPair: StateFlow<Pair<ClockController, ClockController>> = - _previewClockPair + private val _previewClock = MutableStateFlow(Mockito.mock(ClockController::class.java)) + override val previewClock: Flow<ClockController> + get() = _previewClock override val clockEventController: ClockEventController get() = mock() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index dcbd5777489a..de6bfb2f8756 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.data.repository import android.annotation.FloatRange -import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo @@ -48,21 +47,8 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio override val transitions: SharedFlow<TransitionStep> = _transitions init { - _transitions.tryEmit( - TransitionStep( - transitionState = TransitionState.STARTED, - from = KeyguardState.OFF, - to = KeyguardState.LOCKSCREEN, - ) - ) - - _transitions.tryEmit( - TransitionStep( - transitionState = TransitionState.FINISHED, - from = KeyguardState.OFF, - to = KeyguardState.LOCKSCREEN, - ) - ) + // Seed the fake repository with the same initial steps the actual repository uses. + KeyguardTransitionRepositoryImpl.initialTransitionSteps.forEach { _transitions.tryEmit(it) } } /** @@ -207,16 +193,15 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio suspend fun sendTransitionSteps( steps: List<TransitionStep>, testScope: TestScope, - validateStep: Boolean = true + validateSteps: Boolean = true ) { steps.forEach { - sendTransitionStep(step = it, validateStep = validateStep) + sendTransitionStep(step = it, validateStep = validateSteps) testScope.testScheduler.runCurrent() } } override fun startTransition(info: TransitionInfo): UUID? { - Log.i("TEST", "Start transition: ", Exception()) return if (info.animator == null) UUID.randomUUID() else null } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt index dad1887cbd85..f7de5a4c20c7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt @@ -23,11 +23,13 @@ import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInterac import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.keyguardTransitionAnimationFlow by Fixture { KeyguardTransitionAnimationFlow( scope = applicationCoroutineScope, + mainDispatcher = testDispatcher, transitionInteractor = keyguardTransitionInteractor, logger = keyguardTransitionAnimationLogger, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt index 73fd9991945c..709f86426f94 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt @@ -25,6 +25,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInterac import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testScope import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager @@ -50,5 +51,6 @@ val Kosmos.deviceEntryIconViewModel by Fixture { keyguardViewController = { statusBarKeyguardViewManager }, deviceEntryInteractor = deviceEntryInteractor, deviceEntrySourceInteractor = deviceEntrySourceInteractor, + scope = testScope.backgroundScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..f389142554b1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.dreamingToGoneTransitionViewModel by + Kosmos.Fixture { + DreamingToGoneTransitionViewModel( + animationFlow = keyguardTransitionAnimationFlow, + ) + }
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..1b6fa064854d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import kotlinx.coroutines.ExperimentalCoroutinesApi + +var Kosmos.goneToLockscreenTransitionViewModel by Fixture { + GoneToLockscreenTransitionViewModel( + animationFlow = keyguardTransitionAnimationFlow, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index a863edfc5198..b91aafea9c38 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -46,10 +46,13 @@ val Kosmos.keyguardRootViewModel by Fixture { dozingToGoneTransitionViewModel = dozingToGoneTransitionViewModel, dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel, dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel, + dreamingToGoneTransitionViewModel = dreamingToGoneTransitionViewModel, dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel, glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel, goneToAodTransitionViewModel = goneToAodTransitionViewModel, goneToDozingTransitionViewModel = goneToDozingTransitionViewModel, + goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel, + goneToLockscreenTransitionViewModel = goneToLockscreenTransitionViewModel, lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel, lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel, lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt index 85662512a5ee..370afc3b660b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -26,7 +25,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.primaryBouncerToLockscreenTransitionViewModel by Fixture { PrimaryBouncerToLockscreenTransitionViewModel( - deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor, animationFlow = keyguardTransitionAnimationFlow, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index e861892252fa..c879588a1ab7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -19,6 +19,7 @@ package com.android.systemui.kosmos import android.content.applicationContext +import android.os.fakeExecutorHandler import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.bouncerRepository import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor @@ -27,6 +28,7 @@ import com.android.systemui.common.ui.data.repository.fakeConfigurationRepositor import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.communal.data.repository.fakeCommunalRepository import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -65,6 +67,8 @@ class KosmosJavaAdapter( val testScope by lazy { kosmos.testScope } val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic } val fakeSceneContainerFlags by lazy { kosmos.fakeSceneContainerFlags } + val fakeExecutor by lazy { kosmos.fakeExecutor } + val fakeExecutorHandler by lazy { kosmos.fakeExecutorHandler } val configurationRepository by lazy { kosmos.fakeConfigurationRepository } val configurationInteractor by lazy { kosmos.configurationInteractor } val bouncerRepository by lazy { kosmos.bouncerRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt new file mode 100644 index 000000000000..5c17cb95de84 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.data.repository + +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.media.controls.util.mediaFlags + +val Kosmos.mediaDataRepository by Fixture { + MediaDataRepository(mediaFlags = mediaFlags, dumpManager = dumpManager) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt new file mode 100644 index 000000000000..7ce810eb7818 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaFilterRepository by Kosmos.Fixture { MediaFilterRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.kt new file mode 100644 index 000000000000..12a63250fcfc --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaDataCombineLatest by Kosmos.Fixture { MediaDataCombineLatest() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt new file mode 100644 index 000000000000..d56222ed45a4 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.domain.pipeline + +import android.content.applicationContext +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.media.controls.util.mediaUiEventLogger +import com.android.systemui.settings.userTracker +import com.android.systemui.statusbar.notificationLockscreenUserManager +import com.android.systemui.util.time.systemClock +import com.android.systemui.util.wakelock.WakeLockFake + +val Kosmos.mediaDataFilter by + Kosmos.Fixture { + MediaDataFilterImpl( + context = applicationContext, + userTracker = userTracker, + broadcastSender = + BroadcastSender( + applicationContext, + WakeLockFake.Builder(applicationContext), + fakeExecutor + ), + lockscreenUserManager = notificationLockscreenUserManager, + executor = fakeExecutor, + systemClock = systemClock, + logger = mediaUiEventLogger, + mediaFlags = mediaFlags, + mediaFilterRepository = mediaFilterRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt new file mode 100644 index 000000000000..cc1ad1fda6dd --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline + +import android.app.smartspace.SmartspaceManager +import android.content.applicationContext +import android.os.fakeExecutorHandler +import com.android.keyguard.keyguardUpdateMonitor +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.media.controls.data.repository.mediaDataRepository +import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.util.mediaControllerFactory +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.media.controls.util.mediaUiEventLogger +import com.android.systemui.plugins.activityStarter +import com.android.systemui.util.Utils +import com.android.systemui.util.settings.fakeSettings +import com.android.systemui.util.time.systemClock + +val Kosmos.mediaDataProcessor by + Kosmos.Fixture { + MediaDataProcessor( + context = applicationContext, + applicationScope = applicationCoroutineScope, + backgroundDispatcher = testDispatcher, + backgroundExecutor = fakeExecutor, + uiExecutor = fakeExecutor, + foregroundExecutor = fakeExecutor, + handler = fakeExecutorHandler, + mediaControllerFactory = mediaControllerFactory, + broadcastDispatcher = broadcastDispatcher, + dumpManager = dumpManager, + activityStarter = activityStarter, + smartspaceMediaDataProvider = SmartspaceMediaDataProvider(), + useMediaResumption = Utils.useMediaResumption(applicationContext), + useQsMediaPlayer = Utils.useQsMediaPlayer(applicationContext), + systemClock = systemClock, + secureSettings = fakeSettings, + mediaFlags = mediaFlags, + logger = mediaUiEventLogger, + smartspaceManager = SmartspaceManager(applicationContext), + keyguardUpdateMonitor = keyguardUpdateMonitor, + mediaDataRepository = mediaDataRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt new file mode 100644 index 000000000000..b98f557c0c34 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline + +import android.content.applicationContext +import android.media.MediaRouter2Manager +import android.os.fakeExecutorHandler +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.util.localMediaManagerFactory +import com.android.systemui.media.controls.util.mediaControllerFactory +import com.android.systemui.media.muteawait.mediaMuteAwaitConnectionManagerFactory +import com.android.systemui.statusbar.policy.configurationController + +val Kosmos.mediaDeviceManager by + Kosmos.Fixture { + MediaDeviceManager( + context = applicationContext, + controllerFactory = mediaControllerFactory, + localMediaManagerFactory = localMediaManagerFactory, + mr2manager = { MediaRouter2Manager.getInstance(applicationContext) }, + muteAwaitConnectionManagerFactory = mediaMuteAwaitConnectionManagerFactory, + configurationController = configurationController, + localBluetoothManager = { + LocalBluetoothManager.create(applicationContext, fakeExecutorHandler) + }, + fgExecutor = fakeExecutor, + bgExecutor = fakeExecutor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt new file mode 100644 index 000000000000..2a3e84b74369 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline + +import android.content.applicationContext +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.domain.resume.MediaResumeListener +import com.android.systemui.media.controls.domain.resume.resumeMediaBrowserFactory +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.settings.userTracker +import com.android.systemui.tuner.TunerService +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.systemClock + +val Kosmos.mediaResumeListener by + Kosmos.Fixture { + MediaResumeListener( + context = applicationContext, + broadcastDispatcher = broadcastDispatcher, + userTracker = userTracker, + mainExecutor = fakeExecutor, + backgroundExecutor = fakeExecutor, + tunerService = mock<TunerService> {}, + mediaBrowserFactory = resumeMediaBrowserFactory, + dumpManager = dumpManager, + systemClock = systemClock, + mediaFlags = mediaFlags, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt new file mode 100644 index 000000000000..9b02a5b10492 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.domain.pipeline + +import android.content.applicationContext +import android.media.session.MediaSessionManager +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaSessionBasedFilter by + Kosmos.Fixture { + MediaSessionBasedFilter( + context = applicationContext, + sessionManager = MediaSessionManager(applicationContext), + foregroundExecutor = fakeExecutor, + backgroundExecutor = fakeExecutor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt new file mode 100644 index 000000000000..6ec6378e3bc2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline + +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.media.controls.util.mediaControllerFactory +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.util.time.systemClock + +val Kosmos.mediaTimeoutListener by + Kosmos.Fixture { + MediaTimeoutListener( + mediaControllerFactory = mediaControllerFactory, + mainExecutor = fakeExecutor, + logger = MediaTimeoutLogger(logcatLogBuffer("MediaTimeoutLogBuffer")), + statusBarStateController = statusBarStateController, + systemClock = systemClock, + mediaFlags = mediaFlags, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt new file mode 100644 index 000000000000..e5e2affdc49a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.media.controls.data.repository.mediaDataRepository +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.mediaDataCombineLatest +import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter +import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor +import com.android.systemui.media.controls.domain.pipeline.mediaDeviceManager +import com.android.systemui.media.controls.domain.pipeline.mediaResumeListener +import com.android.systemui.media.controls.domain.pipeline.mediaSessionBasedFilter +import com.android.systemui.media.controls.domain.pipeline.mediaTimeoutListener +import com.android.systemui.media.controls.util.mediaFlags + +val Kosmos.mediaCarouselInteractor by + Kosmos.Fixture { + MediaCarouselInteractor( + applicationScope = applicationCoroutineScope, + mediaDataRepository = mediaDataRepository, + mediaDataProcessor = mediaDataProcessor, + mediaTimeoutListener = mediaTimeoutListener, + mediaResumeListener = mediaResumeListener, + mediaSessionBasedFilter = mediaSessionBasedFilter, + mediaDeviceManager = mediaDeviceManager, + mediaDataCombineLatest = mediaDataCombineLatest, + mediaDataFilter = mediaDataFilter, + mediaFilterRepository = mediaFilterRepository, + mediaFlags = mediaFlags, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.kt new file mode 100644 index 000000000000..2621869786d0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.domain.resume + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaBrowserFactory by Kosmos.Fixture { MediaBrowserFactory(applicationContext) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt new file mode 100644 index 000000000000..ed720bd7d7ca --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.resume + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer + +val Kosmos.resumeMediaBrowserFactory by + Kosmos.Fixture { + ResumeMediaBrowserFactory( + applicationContext, + mediaBrowserFactory, + ResumeMediaBrowserLogger(logcatLogBuffer("ResumeMediaLogBuffer")) + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt new file mode 100644 index 000000000000..2e0c9b848d1f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.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.media.controls.util + +import android.content.applicationContext +import android.os.fakeExecutorHandler +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.kosmos.Kosmos + +val Kosmos.localMediaManagerFactory by + Kosmos.Fixture { + LocalMediaManagerFactory( + context = applicationContext, + localBluetoothManager = + LocalBluetoothManager.create(applicationContext, fakeExecutorHandler), + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt new file mode 100644 index 000000000000..1ce6e82f71d8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.util + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaControllerFactory by Kosmos.Fixture { MediaControllerFactory(applicationContext) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.kt new file mode 100644 index 000000000000..6f652f224975 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.util + +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.scene.shared.flag.sceneContainerFlags + +val Kosmos.mediaFlags by + Kosmos.Fixture { + MediaFlags(featureFlags = featureFlagsClassic, sceneContainerFlags = sceneContainerFlags) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.kt new file mode 100644 index 000000000000..b01876d887bb --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.media.controls.util + +import com.android.internal.logging.uiEventLogger +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaUiEventLogger by Kosmos.Fixture { MediaUiEventLogger(uiEventLogger) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt new file mode 100644 index 000000000000..b78bd588869f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.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.media.muteawait + +import android.content.applicationContext +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer + +val Kosmos.mediaMuteAwaitConnectionManagerFactory by + Kosmos.Fixture { + MediaMuteAwaitConnectionManagerFactory( + context = applicationContext, + logger = MediaMuteAwaitLogger(logcatLogBuffer("MediaMuteAwaitLogBuffer")), + mainExecutor = fakeExecutor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt new file mode 100644 index 000000000000..c04c5ed49b33 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.work + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.qsEventLogger +import com.android.systemui.statusbar.policy.PolicyModule + +val Kosmos.qsWorkModeTileConfig by + Kosmos.Fixture { PolicyModule.provideWorkModeTileConfig(qsEventLogger) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt index f4acf4d8fb53..16c5b72a59e0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt @@ -31,6 +31,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.notification.row.NotificationGutsManager import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.ui.viewmodel.windowRootViewVisibilityInteractor @@ -52,6 +53,7 @@ val Kosmos.shadeControllerSceneImpl by notificationStackScrollLayout = mock<NotificationStackScrollLayout>(), deviceEntryInteractor = deviceEntryInteractor, touchLog = mock<LogBuffer>(), + vibratorHelper = mock<VibratorHelper>(), commandQueue = mock<CommandQueue>(), statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>(), notificationShadeWindowController = mock<NotificationShadeWindowController>(), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt new file mode 100644 index 000000000000..2e983a820240 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeHeadsUpRowRepository.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeHeadsUpRowRepository(override val key: String, override val elementKey: Any) : + HeadsUpRowRepository { + override val isPinned: MutableStateFlow<Boolean> = MutableStateFlow(false) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt index 25864aee2136..165c9429c917 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt @@ -18,11 +18,16 @@ package com.android.systemui.statusbar.notification.stack.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationRepository +import com.android.systemui.statusbar.notification.data.repository.HeadsUpRepository +import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() } -class FakeHeadsUpNotificationRepository : HeadsUpNotificationRepository { - override val hasPinnedHeadsUp = MutableStateFlow(false) +class FakeHeadsUpNotificationRepository : HeadsUpRepository { + override val headsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null) + override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> = + MutableStateFlow(emptySet()) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt new file mode 100644 index 000000000000..9be7dfe9a1a9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationsRepositoryExt.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.data.repository + +import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository + +fun FakeHeadsUpNotificationRepository.setNotifications(notifications: List<HeadsUpRowRepository>) { + setNotifications(*notifications.toTypedArray()) +} + +fun FakeHeadsUpNotificationRepository.setNotifications(vararg notifications: HeadsUpRowRepository) { + this.activeHeadsUpRows.value = notifications.toSet() +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt index 546a1e019c6b..5605d1000f4e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt @@ -18,10 +18,12 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.stack.data.repository.notificationStackAppearanceRepository val Kosmos.notificationStackAppearanceInteractor by Fixture { NotificationStackAppearanceInteractor( repository = notificationStackAppearanceRepository, + shadeInteractor = shadeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt index 2de26f13ad73..ee3216b2243d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt @@ -28,6 +28,7 @@ import com.android.systemui.statusbar.notification.notificationActivityStarter import com.android.systemui.statusbar.notification.stack.displaySwitchNotificationsHiderTracker import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel +import com.android.systemui.statusbar.notification.ui.viewbinder.headsUpNotificationViewBinder import com.android.systemui.statusbar.phone.notificationIconAreaController import java.util.Optional @@ -37,6 +38,7 @@ val Kosmos.notificationListViewBinder by Fixture { backgroundDispatcher = testDispatcher, configuration = configurationState, falsingManager = falsingManager, + hunBinder = headsUpNotificationViewBinder, iconAreaController = notificationIconAreaController, loggerOptional = Optional.of(notificationStatsLogger), metricsLogger = metricsLogger, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt index 930a4bbb2daa..c65d0a33cf67 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher @@ -25,6 +26,7 @@ import com.android.systemui.statusbar.notification.domain.interactor.activeNotif import com.android.systemui.statusbar.notification.domain.interactor.seenNotificationsInteractor import com.android.systemui.statusbar.notification.footer.ui.viewmodel.footerViewModel import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.notificationShelfViewModel +import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackInteractor import com.android.systemui.statusbar.policy.domain.interactor.userSetupInteractor import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor @@ -38,6 +40,8 @@ val Kosmos.notificationListViewModel by Fixture { Optional.of(notificationListLoggerViewModel), activeNotificationsInteractor, notificationStackInteractor, + headsUpNotificationInteractor, + keyguardInteractor, remoteInputInteractor, seenNotificationsInteractor, shadeInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt new file mode 100644 index 000000000000..6a995c08ecae --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/ui/viewbinder/HeadsUpNotificationViewBinderKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.ui.viewbinder + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel + +val Kosmos.headsUpNotificationViewBinder by + Kosmos.Fixture { HeadsUpNotificationViewBinder(viewModel = notificationListViewModel) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java index 18b07cf25fbc..59adb11e9054 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java @@ -19,24 +19,65 @@ import android.testing.LeakCheck; import com.android.systemui.statusbar.phone.ManagedProfileController; import com.android.systemui.statusbar.phone.ManagedProfileController.Callback; +import java.util.ArrayList; +import java.util.List; + public class FakeManagedProfileController extends BaseLeakChecker<Callback> implements ManagedProfileController { + + private List<Callback> mCallbackList = new ArrayList<>(); + private boolean mIsEnabled = false; + private boolean mHasActiveProfile = false; + public FakeManagedProfileController(LeakCheck test) { super(test, "profile"); } @Override + public void addCallback(Callback cb) { + mCallbackList.add(cb); + cb.onManagedProfileChanged(); + } + + @Override + public void removeCallback(Callback cb) { + mCallbackList.remove(cb); + } + + @Override public void setWorkModeEnabled(boolean enabled) { + if (mIsEnabled != enabled) { + mIsEnabled = enabled; + for (Callback cb: mCallbackList) { + cb.onManagedProfileChanged(); + } + } } @Override public boolean hasActiveProfile() { - return false; + return mHasActiveProfile; + } + + /** + * Triggers onManagedProfileChanged on callbacks when value flips. + */ + public void setHasActiveProfile(boolean hasActiveProfile) { + if (mHasActiveProfile != hasActiveProfile) { + mHasActiveProfile = hasActiveProfile; + for (Callback cb: mCallbackList) { + cb.onManagedProfileChanged(); + if (!hasActiveProfile) { + cb.onManagedProfileRemoved(); + } + } + } + } @Override public boolean isWorkModeEnabled() { - return false; + return mIsEnabled; } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt new file mode 100644 index 000000000000..5db17243c4e3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.volume + +import android.content.packageManager +import android.content.pm.ApplicationInfo +import android.media.AudioAttributes +import android.media.session.MediaController +import android.media.session.MediaSession +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever + +private const val LOCAL_PACKAGE = "local.test.pkg" +var Kosmos.localMediaController: MediaController by + Kosmos.Fixture { + val appInfo: ApplicationInfo = mock { + whenever(loadLabel(any())).thenReturn("local_media_controller_label") + } + whenever(packageManager.getApplicationInfo(eq(LOCAL_PACKAGE), any<Int>())) + .thenReturn(appInfo) + + val localSessionToken: MediaSession.Token = MediaSession.Token(0, mock {}) + mock { + whenever(packageName).thenReturn(LOCAL_PACKAGE) + whenever(playbackInfo) + .thenReturn( + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, + 0, + 0, + 0, + AudioAttributes.Builder().build(), + "", + ) + ) + whenever(sessionToken).thenReturn(localSessionToken) + } + } + +private const val REMOTE_PACKAGE = "remote.test.pkg" +var Kosmos.remoteMediaController: MediaController by + Kosmos.Fixture { + val appInfo: ApplicationInfo = mock { + whenever(loadLabel(any())).thenReturn("remote_media_controller_label") + } + whenever(packageManager.getApplicationInfo(eq(REMOTE_PACKAGE), any<Int>())) + .thenReturn(appInfo) + + val remoteSessionToken: MediaSession.Token = MediaSession.Token(0, mock {}) + mock { + whenever(packageName).thenReturn(REMOTE_PACKAGE) + whenever(playbackInfo) + .thenReturn( + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, + 0, + 0, + 0, + AudioAttributes.Builder().build(), + "", + ) + ) + whenever(sessionToken).thenReturn(remoteSessionToken) + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt index 3938f77b9c54..fa3a19bae655 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt @@ -18,7 +18,6 @@ package com.android.systemui.volume import android.content.packageManager import android.content.pm.ApplicationInfo -import android.media.session.MediaController import android.os.Handler import android.testing.TestableLooper import com.android.systemui.kosmos.Kosmos @@ -32,11 +31,10 @@ import com.android.systemui.volume.data.repository.FakeLocalMediaRepository import com.android.systemui.volume.data.repository.FakeMediaControllerRepository import com.android.systemui.volume.panel.component.mediaoutput.data.repository.FakeLocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor -var Kosmos.mediaController: MediaController by Kosmos.Fixture { mock {} } - val Kosmos.localMediaRepository by Kosmos.Fixture { FakeLocalMediaRepository() } val Kosmos.localMediaRepositoryFactory: LocalMediaRepositoryFactory by Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } } @@ -56,6 +54,14 @@ val Kosmos.mediaOutputInteractor by }, testScope.backgroundScope, testScope.testScheduler, + mediaControllerRepository, + ) + } + +val Kosmos.mediaDeviceSessionInteractor by + Kosmos.Fixture { + MediaDeviceSessionInteractor( + testScope.testScheduler, Handler(TestableLooper.get(testCase).looper), mediaControllerRepository, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt index 284bd55f15d7..909be7507d34 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt @@ -17,7 +17,6 @@ package com.android.systemui.volume.data.repository import com.android.settingslib.media.MediaDevice -import com.android.settingslib.volume.data.model.RoutingSession import com.android.settingslib.volume.data.repository.LocalMediaRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -25,35 +24,11 @@ import kotlinx.coroutines.flow.asStateFlow class FakeLocalMediaRepository : LocalMediaRepository { - private val volumeBySession: MutableMap<String?, Int> = mutableMapOf() - - private val mutableMediaDevices = MutableStateFlow<List<MediaDevice>>(emptyList()) - override val mediaDevices: StateFlow<List<MediaDevice>> - get() = mutableMediaDevices.asStateFlow() - private val mutableCurrentConnectedDevice = MutableStateFlow<MediaDevice?>(null) override val currentConnectedDevice: StateFlow<MediaDevice?> get() = mutableCurrentConnectedDevice.asStateFlow() - private val mutableRemoteRoutingSessions = MutableStateFlow<List<RoutingSession>>(emptyList()) - override val remoteRoutingSessions: StateFlow<List<RoutingSession>> - get() = mutableRemoteRoutingSessions.asStateFlow() - - fun updateMediaDevices(devices: List<MediaDevice>) { - mutableMediaDevices.value = devices - } - fun updateCurrentConnectedDevice(device: MediaDevice?) { mutableCurrentConnectedDevice.value = device } - - fun updateRemoteRoutingSessions(sessions: List<RoutingSession>) { - mutableRemoteRoutingSessions.value = sessions - } - - fun getSessionVolume(sessionId: String?): Int = volumeBySession.getOrDefault(sessionId, 0) - - override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) { - volumeBySession[sessionId] = volume - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt index 6d52e525d238..8ab5bd903fdf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt @@ -24,11 +24,11 @@ import kotlinx.coroutines.flow.asStateFlow class FakeMediaControllerRepository : MediaControllerRepository { - private val mutableActiveLocalMediaController = MutableStateFlow<MediaController?>(null) - override val activeLocalMediaController: StateFlow<MediaController?> = - mutableActiveLocalMediaController.asStateFlow() + private val mutableActiveSessions = MutableStateFlow<List<MediaController>>(emptyList()) + override val activeSessions: StateFlow<List<MediaController>> + get() = mutableActiveSessions.asStateFlow() - fun setActiveLocalMediaController(controller: MediaController?) { - mutableActiveLocalMediaController.value = controller + fun setActiveSessions(sessions: List<MediaController>) { + mutableActiveSessions.value = sessions } } diff --git a/ravenwood/README.md b/ravenwood/README.md index 8cafb433736f..9c4fda7a50a6 100644 --- a/ravenwood/README.md +++ b/ravenwood/README.md @@ -1,11 +1,9 @@ # Ravenwood -Ravenwood is a lightweight unit testing environment for Android platform code that runs on the host. +Ravenwood is an officially-supported lightweight unit testing environment for Android platform code that runs on the host. Ravenwood’s focus on Android platform use-cases, improved maintainability, and device consistency distinguishes it from Robolectric, which remains a popular choice for app testing. -> **Note:** Active development of Ravenwood has been paused as of March 2024. Existing Ravenwood tests will continue running, but support has moved to a self-service model. - ## Background Executing tests on a typical Android device has substantial overhead, such as flashing the build, waiting for the boot to complete, and retrying tests that fail due to general flakiness. diff --git a/ravenwood/api-maintainers.md b/ravenwood/api-maintainers.md index c059cabd14e2..4b2f96804c97 100644 --- a/ravenwood/api-maintainers.md +++ b/ravenwood/api-maintainers.md @@ -4,7 +4,7 @@ By default, Android APIs aren’t opted-in to Ravenwood, and they default to thr To opt-in to supporting an API under Ravenwood, you can use the inline annotations documented below to customize your API behavior when running under Ravenwood. Because these annotations are inline in the relevant platform source code, they serve as valuable reminders to future API maintainers of Ravenwood support expectations. -> **Note:** Active development of Ravenwood has been paused as of March 2024. Currently supported APIs will continue working, but the addition of new APIs is not currently being supported. There is an allowlist that restricts where Ravenwood-specific annotations can be used, and that allowlist is not being expanded while development is paused. +> **Note:** to ensure that API teams are well-supported during early Ravenwood onboarding, the Ravenwood team is manually maintaining an allow-list of classes that are able to use Ravenwood annotations. Please reach out to ravenwood@ so we can offer design advice and allow-list your APIs. These Ravenwood-specific annotations have no bearing on the status of an API being public, `@SystemApi`, `@TestApi`, `@hide`, etc. Ravenwood annotations are an orthogonal concept that are only consumed by the internal `hoststubgen` tool during a post-processing step that generates the Ravenwood runtime environment. Teams that own APIs can continue to refactor opted-in `@hide` implementation details, as long as the test-visible behavior continues passing. diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java index 81ad31e631fe..61ec7b4bbc72 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java +++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java @@ -383,9 +383,21 @@ public class Parcel_host { // Assume false for now, because we don't support writing FDs yet. return false; } + public static boolean nativeHasFileDescriptorsInRange( long nativePtr, int offset, int length) { // Assume false for now, because we don't support writing FDs yet. return false; } + + public static boolean nativeHasBinders(long nativePtr) { + // Assume false for now, because we don't support adding binders. + return false; + } + + public static boolean nativeHasBindersInRange( + long nativePtr, int offset, int length) { + // Assume false for now, because we don't support writing FDs yet. + return false; + } } diff --git a/services/Android.bp b/services/Android.bp index 98a7979de30a..7bbb42e9a88f 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -253,6 +253,7 @@ java_library { required: [ "libukey2_jni_shared", + "protolog.conf.json.gz", ], lint: { baseline_filename: "lint-baseline.xml", diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 4e14dee8acba..880a68776055 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -993,6 +993,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE), intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE)); } + } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(which)) { + if (!android.view.accessibility.Flags.a11yQsShortcut()) { + return; + } + restoreAccessibilityQsTargets( + intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE)); } } } @@ -2131,6 +2137,29 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub onUserStateChangedLocked(userState); } + /** + * User could configure accessibility shortcut during the SUW before restoring user data. + * Merges the current value and the new value to make sure we don't lost the setting the user's + * preferences of accessibility qs shortcut updated in SUW are not lost. + * + * Called only during settings restore; currently supports only the owner user + * TODO: http://b/22388012 + */ + private void restoreAccessibilityQsTargets(String newValue) { + synchronized (mLock) { + final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM); + final Set<String> mergedTargets = userState.getA11yQsTargets(); + readColonDelimitedStringToSet(newValue, str -> str, mergedTargets, + /* doMerge = */ true); + + userState.updateA11yQsTargetLocked(mergedTargets); + persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_QS_TARGETS, + UserHandle.USER_SYSTEM, mergedTargets, str -> str); + scheduleNotifyClientsOfServicesStateChangeLocked(userState); + onUserStateChangedLocked(userState); + } + } + private int getClientStateLocked(AccessibilityUserState userState) { return userState.getClientStateLocked( mUiAutomationManager.canIntrospect(), diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 9a1d3793e447..7008e8e0f0ba 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -112,6 +112,10 @@ class AccessibilityUserState { * TileService's or the a11y framework tile component names (e.g. * {@link AccessibilityShortcutController#COLOR_INVERSION_TILE_COMPONENT_NAME}) instead of the * A11y Feature's component names. + * <p/> + * In addition, {@link #mA11yTilesInQsPanel} stores what's on the QS Panel, whereas + * {@link #mAccessibilityQsTargets} stores the targets that configured qs as their shortcut and + * also grant full device control permission. */ private final ArraySet<ComponentName> mA11yTilesInQsPanel = new ArraySet<>(); diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java index c570d65d8f57..d30748478741 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java @@ -79,6 +79,8 @@ public class AccessibilityWindowManager { private static int sNextWindowId; + private final Region mTmpRegion = new Region(); + private final Object mLock; private final Handler mHandler; private final WindowManagerInternal mWindowManagerInternal; @@ -613,7 +615,7 @@ public class AccessibilityWindowManager { } // If the window is completely covered by other windows - ignore. - if (unaccountedSpace.quickReject(regionInScreen)) { + if (!mTmpRegion.op(unaccountedSpace, regionInScreen, Region.Op.INTERSECT)) { return false; } diff --git a/services/autofill/features.aconfig b/services/autofill/features.aconfig index 532db126bff2..c130ceef1e08 100644 --- a/services/autofill/features.aconfig +++ b/services/autofill/features.aconfig @@ -16,6 +16,7 @@ flag { flag { name: "autofill_credman_dev_integration" + is_exported: true namespace: "autofill" description: "Guards against Autofill-Credman Phase1 developer integration via new APIs" bug: "320730001" diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java index e4f1d3acce6d..07fcb5042cbc 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java @@ -718,7 +718,9 @@ public final class AutofillManagerService + ", mPccUseFallbackDetection=" + mPccUseFallbackDetection + ", mPccProviderHints=" + mPccProviderHints + ", mAutofillCredmanIntegrationEnabled=" - + mAutofillCredmanIntegrationEnabled); + + mAutofillCredmanIntegrationEnabled + + ", mIsFillFieldsFromCurrentSessionOnly=" + + mIsFillFieldsFromCurrentSessionOnly); } } } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index e1291e5f75ec..272d63d36ede 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -33,8 +33,10 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManagerInternal; import android.content.ComponentName; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.graphics.Rect; import android.metrics.LogMaker; @@ -251,6 +253,31 @@ final class AutofillManagerServiceImpl @Override // from PerUserSystemService protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) throws NameNotFoundException { + final List<ResolveInfo> resolveInfos = + getContext().getPackageManager().queryIntentServicesAsUser( + new Intent(AutofillService.SERVICE_INTERFACE), + // The MATCH_INSTANT flag is added because curret autofill CTS module is + // defined in one apk, which makes the test autofill service installed in a + // instant app when the CTS tests are running in instant app mode. + // TODO: Remove MATCH_INSTANT flag after completing refactoring the CTS module + // to make the test autofill service a separate apk. + PackageManager.GET_META_DATA | PackageManager.MATCH_INSTANT, + mUserId); + boolean serviceHasAutofillIntentFilter = false; + for (ResolveInfo resolveInfo : resolveInfos) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + if (serviceInfo.getComponentName().equals(serviceComponent)) { + serviceHasAutofillIntentFilter = true; + break; + } + } + if (!serviceHasAutofillIntentFilter) { + Slog.w(TAG, + "Autofill service from '" + serviceComponent.getPackageName() + "' does" + + "not have intent filter " + AutofillService.SERVICE_INTERFACE); + throw new SecurityException("Service does not declare intent filter " + + AutofillService.SERVICE_INTERFACE); + } mInfo = new AutofillServiceInfo(getContext(), serviceComponent, mUserId); return mInfo.getServiceInfo(); } @@ -1672,9 +1699,10 @@ final class AutofillManagerServiceImpl @Override // from InlineSuggestionRenderCallbacksImpl public void onServiceDied(@NonNull RemoteInlineSuggestionRenderService service) { - // Don't do anything; eventually the system will bind to it again... Slog.w(TAG, "remote service died: " + service); - mRemoteInlineSuggestionRenderService = null; + synchronized (mLock) { + resetExtServiceLocked(); + } } } diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java deleted file mode 100644 index 0a4148535451..000000000000 --- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java +++ /dev/null @@ -1,567 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.companion; - -import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.SuppressLint; -import android.annotation.UserIdInt; -import android.companion.AssociationInfo; -import android.companion.CompanionDeviceService; -import android.companion.DevicePresenceEvent; -import android.content.ComponentName; -import android.content.Context; -import android.hardware.power.Mode; -import android.os.Handler; -import android.os.ParcelUuid; -import android.os.PowerManagerInternal; -import android.util.Log; -import android.util.Slog; -import android.util.SparseArray; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.infra.PerUser; -import com.android.server.companion.association.AssociationStore; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; -import com.android.server.companion.presence.ObservableUuid; -import com.android.server.companion.presence.ObservableUuidStore; -import com.android.server.companion.utils.PackageUtils; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Manages communication with companion applications via - * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to - * the services, maintaining the connection (the binding), and invoking callback methods such as - * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)}, - * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and - * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the - * application process. - * - * <p> - * The following is the list of the APIs provided by {@link CompanionApplicationController} (to be - * utilized by {@link CompanionDeviceManagerService}): - * <ul> - * <li> {@link #bindCompanionApplication(int, String, boolean)} - * <li> {@link #unbindCompanionApplication(int, String)} - * <li> {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} - * <li> {@link #isCompanionApplicationBound(int, String)} - * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)} - * </ul> - * - * @see CompanionDeviceService - * @see android.companion.ICompanionDeviceService - * @see CompanionDeviceServiceConnector - */ -@SuppressLint("LongLogTag") -public class CompanionApplicationController { - static final boolean DEBUG = false; - private static final String TAG = "CDM_CompanionApplicationController"; - - private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec - - private final @NonNull Context mContext; - private final @NonNull AssociationStore mAssociationStore; - private final @NonNull ObservableUuidStore mObservableUuidStore; - private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor; - private final @NonNull CompanionServicesRegister mCompanionServicesRegister; - - private final PowerManagerInternal mPowerManagerInternal; - - @GuardedBy("mBoundCompanionApplications") - private final @NonNull AndroidPackageMap<List<CompanionDeviceServiceConnector>> - mBoundCompanionApplications; - @GuardedBy("mScheduledForRebindingCompanionApplications") - private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications; - - CompanionApplicationController(Context context, AssociationStore associationStore, - ObservableUuidStore observableUuidStore, - CompanionDevicePresenceMonitor companionDevicePresenceMonitor, - PowerManagerInternal powerManagerInternal) { - mContext = context; - mAssociationStore = associationStore; - mObservableUuidStore = observableUuidStore; - mDevicePresenceMonitor = companionDevicePresenceMonitor; - mPowerManagerInternal = powerManagerInternal; - mCompanionServicesRegister = new CompanionServicesRegister(); - mBoundCompanionApplications = new AndroidPackageMap<>(); - mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>(); - } - - void onPackagesChanged(@UserIdInt int userId) { - mCompanionServicesRegister.invalidate(userId); - } - - /** - * CDM binds to the companion app. - */ - public void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName, - boolean isSelfManaged) { - if (DEBUG) { - Log.i(TAG, "bind() u" + userId + "/" + packageName - + " isSelfManaged=" + isSelfManaged); - } - - final List<ComponentName> companionServices = - mCompanionServicesRegister.forPackage(userId, packageName); - if (companionServices.isEmpty()) { - Slog.w(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": " - + "eligible CompanionDeviceService not found.\n" - + "A CompanionDeviceService should declare an intent-filter for " - + "\"android.companion.CompanionDeviceService\" action and require " - + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission."); - return; - } - - final List<CompanionDeviceServiceConnector> serviceConnectors = new ArrayList<>(); - synchronized (mBoundCompanionApplications) { - if (mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { - if (DEBUG) Log.e(TAG, "u" + userId + "/" + packageName + " is ALREADY bound."); - return; - } - - for (int i = 0; i < companionServices.size(); i++) { - boolean isPrimary = i == 0; - serviceConnectors.add(CompanionDeviceServiceConnector.newInstance(mContext, userId, - companionServices.get(i), isSelfManaged, isPrimary)); - } - - mBoundCompanionApplications.setValueForPackage(userId, packageName, serviceConnectors); - } - - // Set listeners for both Primary and Secondary connectors. - for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) { - serviceConnector.setListener(this::onBinderDied); - } - - // Now "bind" all the connectors: the primary one and the rest of them. - for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) { - serviceConnector.connect(); - } - } - - /** - * CDM unbinds the companion app. - */ - public void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) { - if (DEBUG) Log.i(TAG, "unbind() u" + userId + "/" + packageName); - - final List<CompanionDeviceServiceConnector> serviceConnectors; - - synchronized (mBoundCompanionApplications) { - serviceConnectors = mBoundCompanionApplications.removePackage(userId, packageName); - } - - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.removePackage(userId, packageName); - } - - if (serviceConnectors == null) { - if (DEBUG) { - Log.e(TAG, "unbindCompanionApplication(): " - + "u" + userId + "/" + packageName + " is NOT bound"); - Log.d(TAG, "Stacktrace", new Throwable()); - } - return; - } - - for (CompanionDeviceServiceConnector serviceConnector : serviceConnectors) { - serviceConnector.postUnbind(); - } - } - - /** - * @return whether the companion application is bound now. - */ - public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) { - synchronized (mBoundCompanionApplications) { - return mBoundCompanionApplications.containsValueForPackage(userId, packageName); - } - } - - private void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName, - CompanionDeviceServiceConnector serviceConnector) { - Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName); - - if (isRebindingCompanionApplicationScheduled(userId, packageName)) { - if (DEBUG) { - Log.i(TAG, "CompanionApplication rebinding has been scheduled, skipping " - + serviceConnector.getComponentName()); - } - return; - } - - if (serviceConnector.isPrimary()) { - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.setValueForPackage( - userId, packageName, true); - } - } - - // Rebinding in 10 seconds. - Handler.getMain().postDelayed(() -> - onRebindingCompanionApplicationTimeout(userId, packageName, serviceConnector), - REBIND_TIMEOUT); - } - - private boolean isRebindingCompanionApplicationScheduled( - @UserIdInt int userId, @NonNull String packageName) { - synchronized (mScheduledForRebindingCompanionApplications) { - return mScheduledForRebindingCompanionApplications.containsValueForPackage( - userId, packageName); - } - } - - private void onRebindingCompanionApplicationTimeout( - @UserIdInt int userId, @NonNull String packageName, - @NonNull CompanionDeviceServiceConnector serviceConnector) { - // Re-mark the application is bound. - if (serviceConnector.isPrimary()) { - synchronized (mBoundCompanionApplications) { - if (!mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { - List<CompanionDeviceServiceConnector> serviceConnectors = - Collections.singletonList(serviceConnector); - mBoundCompanionApplications.setValueForPackage(userId, packageName, - serviceConnectors); - } - } - - synchronized (mScheduledForRebindingCompanionApplications) { - mScheduledForRebindingCompanionApplications.removePackage(userId, packageName); - } - } - - serviceConnector.connect(); - } - - /** - * Notify the app that the device appeared. - * - * @deprecated use {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} instead - */ - @Deprecated - public void notifyCompanionApplicationDeviceAppeared(AssociationInfo association) { - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - - Slog.i(TAG, "notifyDevice_Appeared() id=" + association.getId() + " u" + userId - + "/" + packageName); - - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - if (primaryServiceConnector == null) { - Slog.e(TAG, "notify_CompanionApplicationDevice_Appeared(): " - + "u" + userId + "/" + packageName + " is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Log.i(TAG, "Calling onDeviceAppeared to userId=[" + userId + "] package=[" - + packageName + "] associationId=[" + association.getId() + "]"); - - primaryServiceConnector.postOnDeviceAppeared(association); - } - - /** - * Notify the app that the device disappeared. - * - * @deprecated use {@link #notifyCompanionDevicePresenceEvent(AssociationInfo, int)} instead - */ - @Deprecated - public void notifyCompanionApplicationDeviceDisappeared(AssociationInfo association) { - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - - Slog.i(TAG, "notifyDevice_Disappeared() id=" + association.getId() + " u" + userId - + "/" + packageName); - - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - if (primaryServiceConnector == null) { - Slog.e(TAG, "notify_CompanionApplicationDevice_Disappeared(): " - + "u" + userId + "/" + packageName + " is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Log.i(TAG, "Calling onDeviceDisappeared to userId=[" + userId + "] package=[" - + packageName + "] associationId=[" + association.getId() + "]"); - - primaryServiceConnector.postOnDeviceDisappeared(association); - } - - /** - * Notify the app that the device appeared. - */ - public void notifyCompanionDevicePresenceEvent(AssociationInfo association, int event) { - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - final DevicePresenceEvent devicePresenceEvent = - new DevicePresenceEvent(association.getId(), event, null); - - if (primaryServiceConnector == null) { - Slog.e(TAG, "notifyCompanionApplicationDevicePresenceEvent(): " - + "u" + userId + "/" + packageName - + " event=[ " + event + " ] is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=[" - + packageName + "] associationId=[" + association.getId() - + "] event=[" + event + "]"); - - primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent); - } - - /** - * Notify the app that the device disappeared. - */ - public void notifyUuidDevicePresenceEvent(ObservableUuid uuid, int event) { - final int userId = uuid.getUserId(); - final ParcelUuid parcelUuid = uuid.getUuid(); - final String packageName = uuid.getPackageName(); - final CompanionDeviceServiceConnector primaryServiceConnector = - getPrimaryServiceConnector(userId, packageName); - final DevicePresenceEvent devicePresenceEvent = - new DevicePresenceEvent(DevicePresenceEvent.NO_ASSOCIATION, event, parcelUuid); - - if (primaryServiceConnector == null) { - Slog.e(TAG, "notifyApplicationDevicePresenceChanged(): " - + "u" + userId + "/" + packageName - + " event=[ " + event + " ] is NOT bound."); - Slog.e(TAG, "Stacktrace", new Throwable()); - return; - } - - Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=[" - + packageName + "]" + "event= [" + event + "]"); - - primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent); - } - - void dump(@NonNull PrintWriter out) { - out.append("Companion Device Application Controller: \n"); - - synchronized (mBoundCompanionApplications) { - out.append(" Bound Companion Applications: "); - if (mBoundCompanionApplications.size() == 0) { - out.append("<empty>\n"); - } else { - out.append("\n"); - mBoundCompanionApplications.dump(out); - } - } - - out.append(" Companion Applications Scheduled For Rebinding: "); - if (mScheduledForRebindingCompanionApplications.size() == 0) { - out.append("<empty>\n"); - } else { - out.append("\n"); - mScheduledForRebindingCompanionApplications.dump(out); - } - } - - /** - * Rebinding for Self-Managed secondary services OR Non-Self-Managed services. - */ - private void onBinderDied(@UserIdInt int userId, @NonNull String packageName, - @NonNull CompanionDeviceServiceConnector serviceConnector) { - - boolean isPrimary = serviceConnector.isPrimary(); - Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary); - - // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY. - if (isPrimary) { - final List<AssociationInfo> associations = - mAssociationStore.getActiveAssociationsByPackage(userId, packageName); - - for (AssociationInfo association : associations) { - final String deviceProfile = association.getDeviceProfile(); - if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { - Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile); - mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false); - break; - } - } - - synchronized (mBoundCompanionApplications) { - mBoundCompanionApplications.removePackage(userId, packageName); - } - } - - // Second: schedule rebinding if needed. - final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary); - - if (shouldScheduleRebind) { - scheduleRebinding(userId, packageName, serviceConnector); - } - } - - private @Nullable CompanionDeviceServiceConnector getPrimaryServiceConnector( - @UserIdInt int userId, @NonNull String packageName) { - final List<CompanionDeviceServiceConnector> connectors; - synchronized (mBoundCompanionApplications) { - connectors = mBoundCompanionApplications.getValueForPackage(userId, packageName); - } - return connectors != null ? connectors.get(0) : null; - } - - private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) { - // Make sure do not schedule rebind for the case ServiceConnector still gets callback after - // app is uninstalled. - boolean stillAssociated = false; - // Make sure to clean up the state for all the associations - // that associate with this package. - boolean shouldScheduleRebind = false; - boolean shouldScheduleRebindForUuid = false; - final List<ObservableUuid> uuids = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - - for (AssociationInfo ai : - mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) { - final int associationId = ai.getId(); - stillAssociated = true; - if (ai.isSelfManaged()) { - // Do not rebind if primary one is died for selfManaged application. - if (isPrimary - && mDevicePresenceMonitor.isDevicePresent(associationId)) { - mDevicePresenceMonitor.onSelfManagedDeviceReporterBinderDied(associationId); - shouldScheduleRebind = false; - } - // Do not rebind if both primary and secondary services are died for - // selfManaged application. - shouldScheduleRebind = isCompanionApplicationBound(userId, packageName); - } else if (ai.isNotifyOnDeviceNearby()) { - // Always rebind for non-selfManaged devices. - shouldScheduleRebind = true; - } - } - - for (ObservableUuid uuid : uuids) { - if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) { - shouldScheduleRebindForUuid = true; - break; - } - } - - return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid; - } - - private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> { - @Override - public synchronized @NonNull Map<String, List<ComponentName>> forUser( - @UserIdInt int userId) { - return super.forUser(userId); - } - - synchronized @NonNull List<ComponentName> forPackage( - @UserIdInt int userId, @NonNull String packageName) { - return forUser(userId).getOrDefault(packageName, Collections.emptyList()); - } - - synchronized void invalidate(@UserIdInt int userId) { - remove(userId); - } - - @Override - protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) { - return PackageUtils.getCompanionServicesForUser(mContext, userId); - } - } - - /** - * Associates an Android package (defined by userId + packageName) with a value of type T. - */ - private static class AndroidPackageMap<T> extends SparseArray<Map<String, T>> { - - void setValueForPackage( - @UserIdInt int userId, @NonNull String packageName, @NonNull T value) { - Map<String, T> forUser = get(userId); - if (forUser == null) { - forUser = /* Map<String, T> */ new HashMap(); - put(userId, forUser); - } - - forUser.put(packageName, value); - } - - boolean containsValueForPackage(@UserIdInt int userId, @NonNull String packageName) { - final Map<String, ?> forUser = get(userId); - return forUser != null && forUser.containsKey(packageName); - } - - T getValueForPackage(@UserIdInt int userId, @NonNull String packageName) { - final Map<String, T> forUser = get(userId); - return forUser != null ? forUser.get(packageName) : null; - } - - T removePackage(@UserIdInt int userId, @NonNull String packageName) { - final Map<String, T> forUser = get(userId); - if (forUser == null) return null; - return forUser.remove(packageName); - } - - void dump() { - if (size() == 0) { - Log.d(TAG, "<empty>"); - return; - } - - for (int i = 0; i < size(); i++) { - final int userId = keyAt(i); - final Map<String, T> forUser = get(userId); - if (forUser.isEmpty()) { - Log.d(TAG, "u" + userId + ": <empty>"); - } - - for (Map.Entry<String, T> packageValue : forUser.entrySet()) { - final String packageName = packageValue.getKey(); - final T value = packageValue.getValue(); - Log.d(TAG, "u" + userId + "\\" + packageName + " -> " + value); - } - } - } - - private void dump(@NonNull PrintWriter out) { - for (int i = 0; i < size(); i++) { - final int userId = keyAt(i); - final Map<String, T> forUser = get(userId); - if (forUser.isEmpty()) { - out.append(" u").append(String.valueOf(userId)).append(": <empty>\n"); - } - - for (Map.Entry<String, T> packageValue : forUser.entrySet()) { - final String packageName = packageValue.getKey(); - final T value = packageValue.getValue(); - out.append(" u").append(String.valueOf(userId)).append("\\") - .append(packageName).append(" -> ") - .append(value.toString()).append('\n'); - } - } - } - } -} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 712162b2d3b5..f4f6c13e74e4 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -20,15 +20,10 @@ package com.android.server.companion; import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES; import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES; import static android.Manifest.permission.MANAGE_COMPANION_DEVICES; +import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED; import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE; import static android.Manifest.permission.USE_COMPANION_TRANSPORTS; -import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; -import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; import static android.content.pm.PackageManager.CERT_INPUT_SHA256; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.SYSTEM_UID; @@ -42,13 +37,10 @@ import static com.android.server.companion.utils.PackageUtils.getPackageInfo; import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed; import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageCompanionDevice; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; -import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObservingDevicePresenceByUuid; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId; -import static com.android.server.companion.utils.PermissionsUtils.sanitizeWithCallerChecks; import static java.util.Objects.requireNonNull; -import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.MINUTES; import android.annotation.EnforcePermission; @@ -64,7 +56,6 @@ import android.app.PendingIntent; import android.bluetooth.BluetoothDevice; import android.companion.AssociationInfo; import android.companion.AssociationRequest; -import android.companion.DeviceNotAssociatedException; import android.companion.IAssociationRequestCallback; import android.companion.ICompanionDeviceManager; import android.companion.IOnAssociationsChangedListener; @@ -79,7 +70,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; -import android.hardware.power.Mode; import android.net.MacAddress; import android.net.NetworkPolicyManager; import android.os.Binder; @@ -91,7 +81,6 @@ import android.os.PowerExemptionManager; import android.os.PowerManagerInternal; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.util.ArraySet; @@ -118,7 +107,8 @@ import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; import com.android.server.companion.datatransfer.contextsync.CrossDeviceCall; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncControllerCallback; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; +import com.android.server.companion.presence.CompanionAppBinder; +import com.android.server.companion.presence.DevicePresenceProcessor; import com.android.server.companion.presence.ObservableUuid; import com.android.server.companion.presence.ObservableUuidStore; import com.android.server.companion.transport.CompanionTransportManager; @@ -131,10 +121,7 @@ import java.io.PrintWriter; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; @SuppressLint("LongLogTag") @@ -146,10 +133,6 @@ public class CompanionDeviceManagerService extends SystemService { private static final String PREF_FILE_NAME = "companion_device_preferences.xml"; private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done"; - private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW = - "debug.cdm.cdmservice.removal_time_window"; - - private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90); private static final int MAX_CN_LENGTH = 500; private final ActivityTaskManagerInternal mAtmInternal; @@ -165,10 +148,11 @@ public class CompanionDeviceManagerService extends SystemService { private final AssociationRequestsProcessor mAssociationRequestsProcessor; private final SystemDataTransferProcessor mSystemDataTransferProcessor; private final BackupRestoreProcessor mBackupRestoreProcessor; - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; - private final CompanionApplicationController mCompanionAppController; + private final DevicePresenceProcessor mDevicePresenceMonitor; + private final CompanionAppBinder mCompanionAppController; private final CompanionTransportManager mTransportManager; private final DisassociationProcessor mDisassociationProcessor; + private final InactiveAssociationsRemovalService mInactiveAssociationsRemovalService; private final CrossDeviceSyncController mCrossDeviceSyncController; public CompanionDeviceManagerService(Context context) { @@ -185,7 +169,7 @@ public class CompanionDeviceManagerService extends SystemService { mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); final AssociationDiskStore associationDiskStore = new AssociationDiskStore(); - mAssociationStore = new AssociationStore(userManager, associationDiskStore); + mAssociationStore = new AssociationStore(context, userManager, associationDiskStore); mSystemDataTransferRequestStore = new SystemDataTransferRequestStore(); mObservableUuidStore = new ObservableUuidStore(); @@ -196,11 +180,11 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore, mAssociationRequestsProcessor); - mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(userManager, - mAssociationStore, mObservableUuidStore, mDevicePresenceCallback); + mCompanionAppController = new CompanionAppBinder( + context, mAssociationStore, mObservableUuidStore, mPowerManagerInternal); - mCompanionAppController = new CompanionApplicationController( - context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor, + mDevicePresenceMonitor = new DevicePresenceProcessor(context, + mCompanionAppController, userManager, mAssociationStore, mObservableUuidStore, mPowerManagerInternal); mTransportManager = new CompanionTransportManager(context, mAssociationStore); @@ -209,6 +193,9 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore, mPackageManagerInternal, mDevicePresenceMonitor, mCompanionAppController, mSystemDataTransferRequestStore, mTransportManager); + mInactiveAssociationsRemovalService = new InactiveAssociationsRemovalService( + mAssociationStore, mDisassociationProcessor); + mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mPackageManagerInternal, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); @@ -302,181 +289,6 @@ public class CompanionDeviceManagerService extends SystemService { } } - @NonNull - AssociationInfo getAssociationWithCallerChecks( - @UserIdInt int userId, @NonNull String packageName, @NonNull String macAddress) { - AssociationInfo association = mAssociationStore.getFirstAssociationByAddress( - userId, packageName, macAddress); - association = sanitizeWithCallerChecks(getContext(), association); - if (association != null) { - return association; - } else { - throw new IllegalArgumentException("Association does not exist " - + "or the caller does not have permissions to manage it " - + "(ie. it belongs to a different package or a different user)."); - } - } - - @NonNull - AssociationInfo getAssociationWithCallerChecks(int associationId) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - association = sanitizeWithCallerChecks(getContext(), association); - if (association != null) { - return association; - } else { - throw new IllegalArgumentException("Association does not exist " - + "or the caller does not have permissions to manage it " - + "(ie. it belongs to a different package or a different user)."); - } - } - - private void onDeviceAppearedInternal(int associationId) { - if (DEBUG) Log.i(TAG, "onDevice_Appeared_Internal() id=" + associationId); - - final AssociationInfo association = mAssociationStore.getAssociationById(associationId); - if (DEBUG) Log.d(TAG, " association=" + association); - - if (!association.shouldBindWhenPresent()) return; - - bindApplicationIfNeeded(association); - - mCompanionAppController.notifyCompanionApplicationDeviceAppeared(association); - } - - private void onDeviceDisappearedInternal(int associationId) { - if (DEBUG) Log.i(TAG, "onDevice_Disappeared_Internal() id=" + associationId); - - final AssociationInfo association = mAssociationStore.getAssociationById(associationId); - if (DEBUG) Log.d(TAG, " association=" + association); - - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); - return; - } - - if (association.shouldBindWhenPresent()) { - mCompanionAppController.notifyCompanionApplicationDeviceDisappeared(association); - } - } - - private void onDevicePresenceEventInternal(int associationId, int event) { - Slog.i(TAG, "onDevicePresenceEventInternal() id=" + associationId + " event= " + event); - final AssociationInfo association = mAssociationStore.getAssociationById(associationId); - final String packageName = association.getPackageName(); - final int userId = association.getUserId(); - switch (event) { - case EVENT_BLE_APPEARED: - case EVENT_BT_CONNECTED: - case EVENT_SELF_MANAGED_APPEARED: - if (!association.shouldBindWhenPresent()) return; - - bindApplicationIfNeeded(association); - - mCompanionAppController.notifyCompanionDevicePresenceEvent( - association, event); - break; - case EVENT_BLE_DISAPPEARED: - case EVENT_BT_DISCONNECTED: - case EVENT_SELF_MANAGED_DISAPPEARED: - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); - return; - } - if (association.shouldBindWhenPresent()) { - mCompanionAppController.notifyCompanionDevicePresenceEvent( - association, event); - } - // Check if there are other devices associated to the app that are present. - if (shouldBindPackage(userId, packageName)) return; - mCompanionAppController.unbindCompanionApplication(userId, packageName); - break; - default: - Slog.e(TAG, "Event: " + event + "is not supported"); - break; - } - } - - private void onDevicePresenceEventByUuidInternal(ObservableUuid uuid, int event) { - Slog.i(TAG, "onDevicePresenceEventByUuidInternal() id=" + uuid.getUuid() - + "for package=" + uuid.getPackageName() + " event=" + event); - final String packageName = uuid.getPackageName(); - final int userId = uuid.getUserId(); - - switch (event) { - case EVENT_BT_CONNECTED: - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - mCompanionAppController.bindCompanionApplication( - userId, packageName, /*bindImportant*/ false); - - } else if (DEBUG) { - Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound"); - } - - mCompanionAppController.notifyUuidDevicePresenceEvent(uuid, event); - - break; - case EVENT_BT_DISCONNECTED: - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); - return; - } - - mCompanionAppController.notifyUuidDevicePresenceEvent(uuid, event); - // Check if there are other devices associated to the app or the UUID to be - // observed are present. - if (shouldBindPackage(userId, packageName)) return; - - mCompanionAppController.unbindCompanionApplication(userId, packageName); - - break; - default: - Slog.e(TAG, "Event: " + event + "is not supported"); - break; - } - } - - private void bindApplicationIfNeeded(AssociationInfo association) { - final String packageName = association.getPackageName(); - final int userId = association.getUserId(); - // Set bindImportant to true when the association is self-managed to avoid the target - // service being killed. - final boolean bindImportant = association.isSelfManaged(); - if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { - mCompanionAppController.bindCompanionApplication( - userId, packageName, bindImportant); - } else if (DEBUG) { - Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound"); - } - } - - /** - * @return whether the package should be bound (i.e. at least one of the devices associated with - * the package is currently present OR the UUID to be observed by this package is - * currently present). - */ - private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) { - final List<AssociationInfo> packageAssociations = - mAssociationStore.getActiveAssociationsByPackage(userId, packageName); - final List<ObservableUuid> observableUuids = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - - for (AssociationInfo association : packageAssociations) { - if (!association.shouldBindWhenPresent()) continue; - if (mDevicePresenceMonitor.isDevicePresent(association.getId())) return true; - } - - for (ObservableUuid uuid : observableUuids) { - if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) { - return true; - } - } - - return false; - } - private void onPackageRemoveOrDataClearedInternal( @UserIdInt int userId, @NonNull String packageName) { if (DEBUG) { @@ -522,27 +334,8 @@ public class CompanionDeviceManagerService extends SystemService { mBackupRestoreProcessor.restorePendingAssociations(userId, packageName); } - // Revoke associations if the selfManaged companion device does not connect for 3 months. void removeInactiveSelfManagedAssociations() { - final long currentTime = System.currentTimeMillis(); - long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1); - if (removalWindow <= 0) { - // 0 or negative values indicate that the sysprop was never set or should be ignored. - removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT; - } - - for (AssociationInfo association : mAssociationStore.getAssociations()) { - if (!association.isSelfManaged()) continue; - - final boolean isInactive = - currentTime - association.getLastTimeConnectedMs() >= removalWindow; - if (!isInactive) continue; - - final int id = association.getId(); - - Slog.i(TAG, "Removing inactive self-managed association id=" + id); - mDisassociationProcessor.disassociate(id); - } + mInactiveAssociationsRemovalService.removeIdleSelfManagedAssociations(); } public class CompanionDeviceManagerImpl extends ICompanionDeviceManager.Stub { @@ -679,24 +472,15 @@ public class CompanionDeviceManagerService extends SystemService { @Deprecated @Override public void legacyDisassociate(String deviceMacAddress, String packageName, int userId) { - Log.i(TAG, "legacyDisassociate() pkg=u" + userId + "/" + packageName - + ", macAddress=" + deviceMacAddress); - requireNonNull(deviceMacAddress); requireNonNull(packageName); - final AssociationInfo association = - getAssociationWithCallerChecks(userId, packageName, deviceMacAddress); - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(userId, packageName, deviceMacAddress); } @Override public void disassociate(int associationId) { - Slog.i(TAG, "disassociate() associationId=" + associationId); - - final AssociationInfo association = - getAssociationWithCallerChecks(associationId); - mDisassociationProcessor.disassociate(association.getId()); + mDisassociationProcessor.disassociate(associationId); } @Override @@ -758,21 +542,25 @@ public class CompanionDeviceManagerService extends SystemService { } @Override + @Deprecated @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) - public void registerDevicePresenceListenerService(String deviceAddress, - String callingPackage, int userId) throws RemoteException { - registerDevicePresenceListenerService_enforcePermission(); - // TODO: take the userId into account. - registerDevicePresenceListenerActive(callingPackage, deviceAddress, true); + public void legacyStartObservingDevicePresence(String deviceAddress, String callingPackage, + int userId) throws RemoteException { + legacyStartObservingDevicePresence_enforcePermission(); + + mDevicePresenceMonitor.startObservingDevicePresence(userId, callingPackage, + deviceAddress); } @Override + @Deprecated @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) - public void unregisterDevicePresenceListenerService(String deviceAddress, - String callingPackage, int userId) throws RemoteException { - unregisterDevicePresenceListenerService_enforcePermission(); - // TODO: take the userId into account. - registerDevicePresenceListenerActive(callingPackage, deviceAddress, false); + public void legacyStopObservingDevicePresence(String deviceAddress, String callingPackage, + int userId) throws RemoteException { + legacyStopObservingDevicePresence_enforcePermission(); + + mDevicePresenceMonitor.stopObservingDevicePresence(userId, callingPackage, + deviceAddress); } @Override @@ -780,7 +568,8 @@ public class CompanionDeviceManagerService extends SystemService { public void startObservingDevicePresence(ObservingDevicePresenceRequest request, String packageName, int userId) { startObservingDevicePresence_enforcePermission(); - registerDevicePresenceListener(request, packageName, userId, /* active */ true); + + mDevicePresenceMonitor.startObservingDevicePresence(request, packageName, userId); } @Override @@ -788,80 +577,8 @@ public class CompanionDeviceManagerService extends SystemService { public void stopObservingDevicePresence(ObservingDevicePresenceRequest request, String packageName, int userId) { stopObservingDevicePresence_enforcePermission(); - registerDevicePresenceListener(request, packageName, userId, /* active */ false); - } - - private void registerDevicePresenceListener(ObservingDevicePresenceRequest request, - String packageName, int userId, boolean active) { - enforceUsesCompanionDeviceFeature(getContext(), userId, packageName); - enforceCallerIsSystemOr(userId, packageName); - - final int associationId = request.getAssociationId(); - final AssociationInfo associationInfo = mAssociationStore.getAssociationById( - associationId); - final ParcelUuid uuid = request.getUuid(); - if (uuid != null) { - enforceCallerCanObservingDevicePresenceByUuid(getContext()); - if (active) { - startObservingDevicePresenceByUuid(uuid, packageName, userId); - } else { - stopObservingDevicePresenceByUuid(uuid, packageName, userId); - } - } else if (associationInfo == null) { - throw new IllegalArgumentException("App " + packageName - + " is not associated with device " + request.getAssociationId() - + " for user " + userId); - } else { - processDevicePresenceListener( - associationInfo, userId, packageName, active); - } - } - - private void startObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName, - int userId) { - final List<ObservableUuid> observableUuids = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - - for (ObservableUuid observableUuid : observableUuids) { - if (observableUuid.getUuid().equals(uuid)) { - Slog.i(TAG, "The uuid: " + uuid + " for package:" + packageName - + "has been already scheduled for observing"); - return; - } - } - - final ObservableUuid observableUuid = new ObservableUuid(userId, uuid, - packageName, System.currentTimeMillis()); - - mObservableUuidStore.writeObservableUuid(userId, observableUuid); - } - - private void stopObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName, - int userId) { - final List<ObservableUuid> uuidsTobeObserved = - mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); - boolean isScheduledObserving = false; - - for (ObservableUuid observableUuid : uuidsTobeObserved) { - if (observableUuid.getUuid().equals(uuid)) { - isScheduledObserving = true; - break; - } - } - - if (!isScheduledObserving) { - Slog.i(TAG, "The uuid: " + uuid.toString() + " for package:" + packageName - + "has NOT been scheduled for observing yet"); - return; - } - - mObservableUuidStore.removeObservableUuid(userId, uuid, packageName); - mDevicePresenceMonitor.removeCurrentConnectedUuidDevice(uuid); - - if (!shouldBindPackage(userId, packageName)) { - mCompanionAppController.unbindCompanionApplication(userId, packageName); - } + mDevicePresenceMonitor.stopObservingDevicePresence(request, packageName, userId); } @Override @@ -874,8 +591,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public boolean isPermissionTransferUserConsented(String packageName, int userId, int associationId) { - return mSystemDataTransferProcessor.isPermissionTransferUserConsented(packageName, - userId, associationId); + return mSystemDataTransferProcessor.isPermissionTransferUserConsented(associationId); } @Override @@ -891,8 +607,7 @@ public class CompanionDeviceManagerService extends SystemService { ParcelFileDescriptor fd) { attachSystemDataTransport_enforcePermission(); - getAssociationWithCallerChecks(associationId); - mTransportManager.attachSystemDataTransport(packageName, userId, associationId, fd); + mTransportManager.attachSystemDataTransport(associationId, fd); } @Override @@ -900,96 +615,56 @@ public class CompanionDeviceManagerService extends SystemService { public void detachSystemDataTransport(String packageName, int userId, int associationId) { detachSystemDataTransport_enforcePermission(); - getAssociationWithCallerChecks(associationId); - mTransportManager.detachSystemDataTransport(packageName, userId, associationId); + mTransportManager.detachSystemDataTransport(associationId); + } + + @Override + @EnforcePermission(MANAGE_COMPANION_DEVICES) + public void enableSecureTransport(boolean enabled) { + enableSecureTransport_enforcePermission(); + + mTransportManager.enableSecureTransport(enabled); } @Override public void enableSystemDataSync(int associationId, int flags) { - getAssociationWithCallerChecks(associationId); mAssociationRequestsProcessor.enableSystemDataSync(associationId, flags); } @Override public void disableSystemDataSync(int associationId, int flags) { - getAssociationWithCallerChecks(associationId); mAssociationRequestsProcessor.disableSystemDataSync(associationId, flags); } @Override public void enablePermissionsSync(int associationId) { - getAssociationWithCallerChecks(associationId); mSystemDataTransferProcessor.enablePermissionsSync(associationId); } @Override public void disablePermissionsSync(int associationId) { - getAssociationWithCallerChecks(associationId); mSystemDataTransferProcessor.disablePermissionsSync(associationId); } @Override public PermissionSyncRequest getPermissionSyncRequest(int associationId) { - // TODO: temporary fix, will remove soon - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - if (association == null) { - return null; - } - getAssociationWithCallerChecks(associationId); return mSystemDataTransferProcessor.getPermissionSyncRequest(associationId); } @Override - @EnforcePermission(MANAGE_COMPANION_DEVICES) - public void enableSecureTransport(boolean enabled) { - enableSecureTransport_enforcePermission(); - mTransportManager.enableSecureTransport(enabled); - } + @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED) + public void notifySelfManagedDeviceAppeared(int associationId) { + notifySelfManagedDeviceAppeared_enforcePermission(); - @Override - public void notifyDeviceAppeared(int associationId) { - if (DEBUG) Log.i(TAG, "notifyDevice_Appeared() id=" + associationId); - - AssociationInfo association = getAssociationWithCallerChecks(associationId); - if (!association.isSelfManaged()) { - throw new IllegalArgumentException("Association with ID " + associationId - + " is not self-managed. notifyDeviceAppeared(int) can only be called for" - + " self-managed associations."); - } - // AssociationInfo class is immutable: create a new AssociationInfo object with updated - // timestamp. - association = (new AssociationInfo.Builder(association)) - .setLastTimeConnected(System.currentTimeMillis()) - .build(); - mAssociationStore.updateAssociation(association); - - mDevicePresenceMonitor.onSelfManagedDeviceConnected(associationId); - - final String deviceProfile = association.getDeviceProfile(); - if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { - Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile); - mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, true); - } + mDevicePresenceMonitor.notifySelfManagedDevicePresenceEvent(associationId, true); } @Override - public void notifyDeviceDisappeared(int associationId) { - if (DEBUG) Log.i(TAG, "notifyDevice_Disappeared() id=" + associationId); - - final AssociationInfo association = getAssociationWithCallerChecks(associationId); - if (!association.isSelfManaged()) { - throw new IllegalArgumentException("Association with ID " + associationId - + " is not self-managed. notifyDeviceAppeared(int) can only be called for" - + " self-managed associations."); - } - - mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId); + @EnforcePermission(REQUEST_COMPANION_SELF_MANAGED) + public void notifySelfManagedDeviceDisappeared(int associationId) { + notifySelfManagedDeviceDisappeared_enforcePermission(); - final String deviceProfile = association.getDeviceProfile(); - if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { - Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile); - mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false); - } + mDevicePresenceMonitor.notifySelfManagedDevicePresenceEvent(associationId, false); } @Override @@ -997,66 +672,6 @@ public class CompanionDeviceManagerService extends SystemService { return mCompanionAppController.isCompanionApplicationBound(userId, packageName); } - private void registerDevicePresenceListenerActive(String packageName, String deviceAddress, - boolean active) throws RemoteException { - if (DEBUG) { - Log.i(TAG, "registerDevicePresenceListenerActive()" - + " active=" + active - + " deviceAddress=" + deviceAddress); - } - final int userId = getCallingUserId(); - enforceCallerIsSystemOr(userId, packageName); - - AssociationInfo association = mAssociationStore.getFirstAssociationByAddress( - userId, packageName, deviceAddress); - - if (association == null) { - throw new RemoteException(new DeviceNotAssociatedException("App " + packageName - + " is not associated with device " + deviceAddress - + " for user " + userId)); - } - - processDevicePresenceListener(association, userId, packageName, active); - } - - private void processDevicePresenceListener(AssociationInfo association, - int userId, String packageName, boolean active) { - // If already at specified state, then no-op. - if (active == association.isNotifyOnDeviceNearby()) { - if (DEBUG) Log.d(TAG, "Device presence listener is already at desired state."); - return; - } - - // AssociationInfo class is immutable: create a new AssociationInfo object with updated - // flag. - association = (new AssociationInfo.Builder(association)) - .setNotifyOnDeviceNearby(active) - .build(); - // Do not need to call {@link BleCompanionDeviceScanner#restartScan()} since it will - // trigger {@link BleCompanionDeviceScanner#restartScan(int, AssociationInfo)} when - // an application sets/unsets the mNotifyOnDeviceNearby flag. - mAssociationStore.updateAssociation(association); - - int associationId = association.getId(); - // If device is already present, then trigger callback. - if (active && mDevicePresenceMonitor.isDevicePresent(associationId)) { - Slog.i(TAG, "Device is already present. Triggering callback."); - if (mDevicePresenceMonitor.isBlePresent(associationId) - || mDevicePresenceMonitor.isSimulatePresent(associationId)) { - onDeviceAppearedInternal(associationId); - onDevicePresenceEventInternal(associationId, EVENT_BLE_APPEARED); - } else if (mDevicePresenceMonitor.isBtConnected(associationId)) { - onDevicePresenceEventInternal(associationId, EVENT_BT_CONNECTED); - } - } - - // If last listener is unregistered, then unbind application. - if (!active && !shouldBindPackage(userId, packageName)) { - if (DEBUG) Log.d(TAG, "Last listener unregistered. Unbinding application."); - mCompanionAppController.unbindCompanionApplication(userId, packageName); - } - } - @Override @EnforcePermission(ASSOCIATE_COMPANION_DEVICES) public void createAssociation(String packageName, String macAddress, int userId, @@ -1070,7 +685,8 @@ public class CompanionDeviceManagerService extends SystemService { } final MacAddress macAddressObj = MacAddress.fromString(macAddress); - createNewAssociation(userId, packageName, macAddressObj, null, null, false); + mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddressObj, + null, null, null, false, null, null); } private void checkCanCallNotificationApi(String callingPackage, int userId) { @@ -1099,9 +715,7 @@ public class CompanionDeviceManagerService extends SystemService { @Override public void setAssociationTag(int associationId, String tag) { - AssociationInfo association = getAssociationWithCallerChecks(associationId); - association = (new AssociationInfo.Builder(association)).setTag(tag).build(); - mAssociationStore.updateAssociation(association); + mAssociationRequestsProcessor.setAssociationTag(associationId, tag); } @Override @@ -1146,14 +760,6 @@ public class CompanionDeviceManagerService extends SystemService { } } - void createNewAssociation(@UserIdInt int userId, @NonNull String packageName, - @Nullable MacAddress macAddress, @Nullable CharSequence displayName, - @Nullable String deviceProfile, boolean isSelfManaged) { - mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress, - displayName, deviceProfile, /* associatedDevice */ null, isSelfManaged, - /* callback */ null, /* resultReceiver */ null); - } - /** * Update special access for the association's package */ @@ -1169,8 +775,6 @@ public class CompanionDeviceManagerService extends SystemService { return; } - Slog.i(TAG, "Updating special access for package=[" + packageInfo.packageName + "]..."); - if (containsEither(packageInfo.requestedPermissions, android.Manifest.permission.RUN_IN_BACKGROUND, android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) { @@ -1280,29 +884,6 @@ public class CompanionDeviceManagerService extends SystemService { } }; - private final CompanionDevicePresenceMonitor.Callback mDevicePresenceCallback = - new CompanionDevicePresenceMonitor.Callback() { - @Override - public void onDeviceAppeared(int associationId) { - onDeviceAppearedInternal(associationId); - } - - @Override - public void onDeviceDisappeared(int associationId) { - onDeviceDisappearedInternal(associationId); - } - - @Override - public void onDevicePresenceEvent(int associationId, int event) { - onDevicePresenceEventInternal(associationId, event); - } - - @Override - public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) { - onDevicePresenceEventByUuidInternal(uuid, event); - } - }; - private final PackageMonitor mPackageMonitor = new PackageMonitor() { @Override public void onPackageRemoved(String packageName, int uid) { @@ -1315,7 +896,7 @@ public class CompanionDeviceManagerService extends SystemService { } @Override - public void onPackageModified(String packageName) { + public void onPackageModified(@NonNull String packageName) { onPackageModifiedInternal(getChangingUserId(), packageName); } @@ -1325,28 +906,12 @@ public class CompanionDeviceManagerService extends SystemService { } }; - private static Map<String, Set<Integer>> deepUnmodifiableCopy(Map<String, Set<Integer>> orig) { - final Map<String, Set<Integer>> copy = new HashMap<>(); - - for (Map.Entry<String, Set<Integer>> entry : orig.entrySet()) { - final Set<Integer> valueCopy = new HashSet<>(entry.getValue()); - copy.put(entry.getKey(), Collections.unmodifiableSet(valueCopy)); - } - - return Collections.unmodifiableMap(copy); - } - private static <T> boolean containsEither(T[] array, T a, T b) { return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b); } private class LocalService implements CompanionDeviceManagerServiceInternal { @Override - public void removeInactiveSelfManagedAssociations() { - CompanionDeviceManagerService.this.removeInactiveSelfManagedAssociations(); - } - - @Override public void registerCallMetadataSyncCallback(CrossDeviceSyncControllerCallback callback, @CrossDeviceSyncControllerCallback.Type int type) { if (CompanionDeviceConfig.isEnabled( diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java index cdf832f8c788..e3b4c95a7dab 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerServiceInternal.java @@ -28,11 +28,6 @@ import java.util.Collection; */ public interface CompanionDeviceManagerServiceInternal { /** - * @see CompanionDeviceManagerService#removeInactiveSelfManagedAssociations - */ - void removeInactiveSelfManagedAssociations(); - - /** * Registers a callback from an InCallService / ConnectionService to CDM to process sync * requests and perform call control actions. */ diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index a7a73cb6bddb..a78938400a1e 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -18,8 +18,6 @@ package com.android.server.companion; import static android.companion.CompanionDeviceManager.MESSAGE_REQUEST_CONTEXT_SYNC; -import static com.android.server.companion.utils.PermissionsUtils.sanitizeWithCallerChecks; - import android.companion.AssociationInfo; import android.companion.ContextSyncMessage; import android.companion.Flags; @@ -38,7 +36,7 @@ import com.android.server.companion.association.DisassociationProcessor; import com.android.server.companion.datatransfer.SystemDataTransferProcessor; import com.android.server.companion.datatransfer.contextsync.BitmapUtils; import com.android.server.companion.datatransfer.contextsync.CrossDeviceSyncController; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; +import com.android.server.companion.presence.DevicePresenceProcessor; import com.android.server.companion.presence.ObservableUuid; import com.android.server.companion.transport.CompanionTransportManager; @@ -51,7 +49,7 @@ class CompanionDeviceShellCommand extends ShellCommand { private final CompanionDeviceManagerService mService; private final DisassociationProcessor mDisassociationProcessor; private final AssociationStore mAssociationStore; - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; + private final DevicePresenceProcessor mDevicePresenceProcessor; private final CompanionTransportManager mTransportManager; private final SystemDataTransferProcessor mSystemDataTransferProcessor; @@ -60,7 +58,7 @@ class CompanionDeviceShellCommand extends ShellCommand { CompanionDeviceShellCommand(CompanionDeviceManagerService service, AssociationStore associationStore, - CompanionDevicePresenceMonitor devicePresenceMonitor, + DevicePresenceProcessor devicePresenceProcessor, CompanionTransportManager transportManager, SystemDataTransferProcessor systemDataTransferProcessor, AssociationRequestsProcessor associationRequestsProcessor, @@ -68,7 +66,7 @@ class CompanionDeviceShellCommand extends ShellCommand { DisassociationProcessor disassociationProcessor) { mService = service; mAssociationStore = associationStore; - mDevicePresenceMonitor = devicePresenceMonitor; + mDevicePresenceProcessor = devicePresenceProcessor; mTransportManager = transportManager; mSystemDataTransferProcessor = systemDataTransferProcessor; mAssociationRequestsProcessor = associationRequestsProcessor; @@ -85,7 +83,7 @@ class CompanionDeviceShellCommand extends ShellCommand { if ("simulate-device-event".equals(cmd) && Flags.devicePresence()) { associationId = getNextIntArgRequired(); int event = getNextIntArgRequired(); - mDevicePresenceMonitor.simulateDeviceEvent(associationId, event); + mDevicePresenceProcessor.simulateDeviceEvent(associationId, event); return 0; } @@ -97,7 +95,7 @@ class CompanionDeviceShellCommand extends ShellCommand { ObservableUuid observableUuid = new ObservableUuid( userId, ParcelUuid.fromString(uuid), packageName, System.currentTimeMillis()); - mDevicePresenceMonitor.simulateDeviceEventByUuid(observableUuid, event); + mDevicePresenceProcessor.simulateDeviceEventByUuid(observableUuid, event); return 0; } @@ -124,8 +122,9 @@ class CompanionDeviceShellCommand extends ShellCommand { String address = getNextArgRequired(); String deviceProfile = getNextArg(); final MacAddress macAddress = MacAddress.fromString(address); - mService.createNewAssociation(userId, packageName, macAddress, - /* displayName= */ deviceProfile, deviceProfile, false); + mAssociationRequestsProcessor.createAssociation(userId, packageName, macAddress, + deviceProfile, deviceProfile, /* associatedDevice */ null, false, + /* callback */ null, /* resultReceiver */ null); } break; @@ -134,8 +133,13 @@ class CompanionDeviceShellCommand extends ShellCommand { final String packageName = getNextArgRequired(); final String address = getNextArgRequired(); final AssociationInfo association = - mService.getAssociationWithCallerChecks(userId, packageName, address); - mDisassociationProcessor.disassociate(association.getId()); + mAssociationStore.getFirstAssociationByAddress(userId, packageName, + address); + if (association == null) { + out.println("Association doesn't exist."); + } else { + mDisassociationProcessor.disassociate(association.getId()); + } } break; @@ -144,9 +148,7 @@ class CompanionDeviceShellCommand extends ShellCommand { final List<AssociationInfo> userAssociations = mAssociationStore.getAssociationsByUser(userId); for (AssociationInfo association : userAssociations) { - if (sanitizeWithCallerChecks(mService.getContext(), association) != null) { - mDisassociationProcessor.disassociate(association.getId()); - } + mDisassociationProcessor.disassociate(association.getId()); } } break; @@ -157,12 +159,12 @@ class CompanionDeviceShellCommand extends ShellCommand { case "simulate-device-appeared": associationId = getNextIntArgRequired(); - mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 0); + mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 0); break; case "simulate-device-disappeared": associationId = getNextIntArgRequired(); - mDevicePresenceMonitor.simulateDeviceEvent(associationId, /* event */ 1); + mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 1); break; case "get-backup-payload": { @@ -410,10 +412,9 @@ class CompanionDeviceShellCommand extends ShellCommand { pw.println(" Remove an existing Association."); pw.println(" disassociate-all USER_ID"); pw.println(" Remove all Associations for a user."); - pw.println(" clear-association-memory-cache"); + pw.println(" refresh-cache"); pw.println(" Clear the in-memory association cache and reload all association "); - pw.println(" information from persistent storage. USE FOR DEBUGGING PURPOSES ONLY."); - pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); + pw.println(" information from disk. USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); pw.println(" simulate-device-appeared ASSOCIATION_ID"); pw.println(" Make CDM act as if the given companion device has appeared."); diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java index a02d9f912bcd..a18776e67200 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java +++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java @@ -145,7 +145,8 @@ public class AssociationRequestsProcessor { /** * Handle incoming {@link AssociationRequest}s, sent via - * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)} + * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, + * IAssociationRequestCallback, String, int)} */ public void processNewAssociationRequest(@NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @@ -212,7 +213,8 @@ public class AssociationRequestsProcessor { // 2b.4. Send the PendingIntent back to the app. try { callback.onAssociationPending(pendingIntent); - } catch (RemoteException ignore) { } + } catch (RemoteException ignore) { + } } /** @@ -252,7 +254,8 @@ public class AssociationRequestsProcessor { // forward it back to the application via the callback. try { callback.onFailure(e.getMessage()); - } catch (RemoteException ignore) { } + } catch (RemoteException ignore) { + } return; } @@ -322,7 +325,8 @@ public class AssociationRequestsProcessor { * Enable system data sync. */ public void enableSystemDataSync(int associationId, int flags) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) .setSystemDataSyncFlags(association.getSystemDataSyncFlags() | flags).build(); mAssociationStore.updateAssociation(updated); @@ -332,12 +336,23 @@ public class AssociationRequestsProcessor { * Disable system data sync. */ public void disableSystemDataSync(int associationId, int flags) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); AssociationInfo updated = (new AssociationInfo.Builder(association)) .setSystemDataSyncFlags(association.getSystemDataSyncFlags() & (~flags)).build(); mAssociationStore.updateAssociation(updated); } + /** + * Set association tag. + */ + public void setAssociationTag(int associationId, String tag) { + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + association = (new AssociationInfo.Builder(association)).setTag(tag).build(); + mAssociationStore.updateAssociation(association); + } + private void sendCallbackAndFinish(@Nullable AssociationInfo association, @Nullable IAssociationRequestCallback callback, @Nullable ResultReceiver resultReceiver) { @@ -396,14 +411,14 @@ public class AssociationRequestsProcessor { // If the application already has a pending association request, that PendingIntent // will be cancelled except application wants to cancel the request by the system. return Binder.withCleanCallingIdentity(() -> - PendingIntent.getActivityAsUser( - mContext, /*requestCode */ packageUid, intent, - FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, - ActivityOptions.makeBasic() - .setPendingIntentCreatorBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) - .toBundle(), - UserHandle.CURRENT) + PendingIntent.getActivityAsUser( + mContext, /*requestCode */ packageUid, intent, + FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, + ActivityOptions.makeBasic() + .setPendingIntentCreatorBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED) + .toBundle(), + UserHandle.CURRENT) ); } diff --git a/services/companion/java/com/android/server/companion/association/AssociationStore.java b/services/companion/java/com/android/server/companion/association/AssociationStore.java index edebb55233d0..ae2b70852a35 100644 --- a/services/companion/java/com/android/server/companion/association/AssociationStore.java +++ b/services/companion/java/com/android/server/companion/association/AssociationStore.java @@ -18,6 +18,7 @@ package com.android.server.companion.association; import static com.android.server.companion.utils.MetricUtils.logCreateAssociation; import static com.android.server.companion.utils.MetricUtils.logRemoveAssociation; +import static com.android.server.companion.utils.PermissionsUtils.checkCallerCanManageAssociationsForPackage; import android.annotation.IntDef; import android.annotation.NonNull; @@ -26,6 +27,7 @@ import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.companion.AssociationInfo; import android.companion.IOnAssociationsChangedListener; +import android.content.Context; import android.content.pm.UserInfo; import android.net.MacAddress; import android.os.Binder; @@ -57,21 +59,22 @@ import java.util.concurrent.Executors; @SuppressLint("LongLogTag") public class AssociationStore { - @IntDef(prefix = { "CHANGE_TYPE_" }, value = { + @IntDef(prefix = {"CHANGE_TYPE_"}, value = { CHANGE_TYPE_ADDED, CHANGE_TYPE_REMOVED, CHANGE_TYPE_UPDATED_ADDRESS_CHANGED, CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED, }) @Retention(RetentionPolicy.SOURCE) - public @interface ChangeType {} + public @interface ChangeType { + } public static final int CHANGE_TYPE_ADDED = 0; public static final int CHANGE_TYPE_REMOVED = 1; public static final int CHANGE_TYPE_UPDATED_ADDRESS_CHANGED = 2; public static final int CHANGE_TYPE_UPDATED_ADDRESS_UNCHANGED = 3; - /** Listener for any changes to associations. */ + /** Listener for any changes to associations. */ public interface OnChangeListener { /** * Called when there are association changes. @@ -100,25 +103,30 @@ public class AssociationStore { /** * Called when an association is added. */ - default void onAssociationAdded(AssociationInfo association) {} + default void onAssociationAdded(AssociationInfo association) { + } /** * Called when an association is removed. */ - default void onAssociationRemoved(AssociationInfo association) {} + default void onAssociationRemoved(AssociationInfo association) { + } /** * Called when an association is updated. */ - default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) {} + default void onAssociationUpdated(AssociationInfo association, boolean addressChanged) { + } } private static final String TAG = "CDM_AssociationStore"; - private final Object mLock = new Object(); - + private final Context mContext; + private final UserManager mUserManager; + private final AssociationDiskStore mDiskStore; private final ExecutorService mExecutor; + private final Object mLock = new Object(); @GuardedBy("mLock") private boolean mPersisted = false; @GuardedBy("mLock") @@ -132,10 +140,9 @@ public class AssociationStore { private final RemoteCallbackList<IOnAssociationsChangedListener> mRemoteListeners = new RemoteCallbackList<>(); - private final UserManager mUserManager; - private final AssociationDiskStore mDiskStore; - - public AssociationStore(UserManager userManager, AssociationDiskStore diskStore) { + public AssociationStore(Context context, UserManager userManager, + AssociationDiskStore diskStore) { + mContext = context; mUserManager = userManager; mDiskStore = diskStore; mExecutor = Executors.newSingleThreadExecutor(); @@ -202,7 +209,7 @@ public class AssociationStore { synchronized (mLock) { if (mIdToAssociationMap.containsKey(id)) { - Slog.e(TAG, "Association with id=[" + id + "] already exists."); + Slog.e(TAG, "Association id=[" + id + "] already exists."); return; } @@ -449,6 +456,26 @@ public class AssociationStore { } /** + * Get association by id with caller checks. + */ + @NonNull + public AssociationInfo getAssociationWithCallerChecks(int associationId) { + AssociationInfo association = getAssociationById(associationId); + if (association == null) { + throw new IllegalArgumentException( + "getAssociationWithCallerChecks() Association id=[" + associationId + + "] doesn't exist."); + } + if (checkCallerCanManageAssociationsForPackage(mContext, association.getUserId(), + association.getPackageName())) { + return association; + } + + throw new IllegalArgumentException( + "The caller can't interact with the association id=[" + associationId + "]."); + } + + /** * Register a local listener for association changes. */ public void registerLocalListener(@NonNull OnChangeListener listener) { diff --git a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java index ec8977918c56..20de1210dd9d 100644 --- a/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java +++ b/services/companion/java/com/android/server/companion/association/DisassociationProcessor.java @@ -33,18 +33,19 @@ import android.os.Binder; import android.os.UserHandle; import android.util.Slog; -import com.android.server.companion.CompanionApplicationController; import com.android.server.companion.datatransfer.SystemDataTransferRequestStore; -import com.android.server.companion.presence.CompanionDevicePresenceMonitor; +import com.android.server.companion.presence.CompanionAppBinder; +import com.android.server.companion.presence.DevicePresenceProcessor; import com.android.server.companion.transport.CompanionTransportManager; /** - * A class response for Association removal. + * This class responsible for disassociation. */ @SuppressLint("LongLogTag") public class DisassociationProcessor { private static final String TAG = "CDM_DisassociationProcessor"; + @NonNull private final Context mContext; @NonNull @@ -52,11 +53,11 @@ public class DisassociationProcessor { @NonNull private final PackageManagerInternal mPackageManagerInternal; @NonNull - private final CompanionDevicePresenceMonitor mDevicePresenceMonitor; + private final DevicePresenceProcessor mDevicePresenceMonitor; @NonNull private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; @NonNull - private final CompanionApplicationController mCompanionAppController; + private final CompanionAppBinder mCompanionAppController; @NonNull private final CompanionTransportManager mTransportManager; private final OnPackageVisibilityChangeListener mOnPackageVisibilityChangeListener; @@ -66,8 +67,8 @@ public class DisassociationProcessor { @NonNull ActivityManager activityManager, @NonNull AssociationStore associationStore, @NonNull PackageManagerInternal packageManager, - @NonNull CompanionDevicePresenceMonitor devicePresenceMonitor, - @NonNull CompanionApplicationController applicationController, + @NonNull DevicePresenceProcessor devicePresenceMonitor, + @NonNull CompanionAppBinder applicationController, @NonNull SystemDataTransferRequestStore systemDataTransferRequestStore, @NonNull CompanionTransportManager companionTransportManager) { mContext = context; @@ -89,11 +90,7 @@ public class DisassociationProcessor { public void disassociate(int id) { Slog.i(TAG, "Disassociating id=[" + id + "]..."); - final AssociationInfo association = mAssociationStore.getAssociationById(id); - if (association == null) { - Slog.e(TAG, "Can't disassociate id=[" + id + "]. It doesn't exist."); - return; - } + final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks(id); final int userId = association.getUserId(); final String packageName = association.getPackageName(); @@ -118,12 +115,12 @@ public class DisassociationProcessor { return; } + // Detach transport if exists + mTransportManager.detachSystemDataTransport(id); + // Association cleanup. - mAssociationStore.removeAssociation(association.getId()); mSystemDataTransferRequestStore.removeRequestsByAssociationId(userId, id); - - // Detach transport if exists - mTransportManager.detachSystemDataTransport(packageName, userId, id); + mAssociationStore.removeAssociation(association.getId()); // If role is not in use by other associations, revoke the role. // Do not need to remove the system role since it was pre-granted by the system. @@ -147,6 +144,24 @@ public class DisassociationProcessor { } } + /** + * @deprecated Use {@link #disassociate(int)} instead. + */ + @Deprecated + public void disassociate(int userId, String packageName, String macAddress) { + AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId, + packageName, macAddress); + + if (association == null) { + throw new IllegalArgumentException( + "Association for mac address=[" + macAddress + "] doesn't exist"); + } + + mAssociationStore.getAssociationWithCallerChecks(association.getId()); + + disassociate(association.getId()); + } + @SuppressLint("MissingPermission") private int getPackageProcessImportance(@UserIdInt int userId, @NonNull String packageName) { return Binder.withCleanCallingIdentity(() -> { @@ -163,7 +178,7 @@ public class DisassociationProcessor { () -> mActivityManager.addOnUidImportanceListener( mOnPackageVisibilityChangeListener, ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE)); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException e) { Slog.e(TAG, "Failed to start listening to uid importance changes."); } } diff --git a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java index f28731548dcc..b52904aa5301 100644 --- a/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java +++ b/services/companion/java/com/android/server/companion/association/InactiveAssociationsRemovalService.java @@ -22,15 +22,14 @@ import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; +import android.companion.AssociationInfo; import android.content.ComponentName; import android.content.Context; +import android.os.SystemProperties; import android.util.Slog; -import com.android.server.LocalServices; -import com.android.server.companion.CompanionDeviceManagerServiceInternal; - /** - * A Job Service responsible for clean up idle self-managed associations. + * A Job Service responsible for clean up self-managed associations if it's idle for 90 days. * * The job will be executed only if the device is charging and in idle mode due to the application * will be killed if association/role are revoked. See {@link DisassociationProcessor} @@ -41,14 +40,25 @@ public class InactiveAssociationsRemovalService extends JobService { private static final String JOB_NAMESPACE = "companion"; private static final int JOB_ID = 1; private static final long ONE_DAY_INTERVAL = DAYS.toMillis(1); + private static final String SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW = + "debug.cdm.cdmservice.removal_time_window"; + private static final long ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT = DAYS.toMillis(90); + + private final AssociationStore mAssociationStore; + private final DisassociationProcessor mDisassociationProcessor; + + public InactiveAssociationsRemovalService(AssociationStore associationStore, + DisassociationProcessor disassociationProcessor) { + mAssociationStore = associationStore; + mDisassociationProcessor = disassociationProcessor; + } @Override public boolean onStartJob(final JobParameters params) { Slog.i(TAG, "Execute the Association Removal job"); - // Special policy for selfManaged that need to revoke associations if the device - // does not connect for 90 days. - LocalServices.getService(CompanionDeviceManagerServiceInternal.class) - .removeInactiveSelfManagedAssociations(); + + removeIdleSelfManagedAssociations(); + jobFinished(params, false); return true; } @@ -77,4 +87,29 @@ public class InactiveAssociationsRemovalService extends JobService { .build(); jobScheduler.schedule(job); } + + /** + * Remove idle self-managed associations. + */ + public void removeIdleSelfManagedAssociations() { + final long currentTime = System.currentTimeMillis(); + long removalWindow = SystemProperties.getLong(SYS_PROP_DEBUG_REMOVAL_TIME_WINDOW, -1); + if (removalWindow <= 0) { + // 0 or negative values indicate that the sysprop was never set or should be ignored. + removalWindow = ASSOCIATION_REMOVAL_TIME_WINDOW_DEFAULT; + } + + for (AssociationInfo association : mAssociationStore.getAssociations()) { + if (!association.isSelfManaged()) continue; + + final boolean isInactive = + currentTime - association.getLastTimeConnectedMs() >= removalWindow; + if (!isInactive) continue; + + final int id = association.getId(); + + Slog.i(TAG, "Removing inactive self-managed association id=" + id); + mDisassociationProcessor.disassociate(id); + } + } } diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java index c5ca0bf7e9c5..9069689ee5eb 100644 --- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java +++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java @@ -31,7 +31,6 @@ import android.annotation.UserIdInt; import android.app.ActivityOptions; import android.app.PendingIntent; import android.companion.AssociationInfo; -import android.companion.DeviceNotAssociatedException; import android.companion.IOnMessageReceivedListener; import android.companion.ISystemDataTransferCallback; import android.companion.datatransfer.PermissionSyncRequest; @@ -56,7 +55,6 @@ import com.android.server.companion.CompanionDeviceManagerService; import com.android.server.companion.association.AssociationStore; import com.android.server.companion.transport.CompanionTransportManager; import com.android.server.companion.utils.PackageUtils; -import com.android.server.companion.utils.PermissionsUtils; import java.util.List; import java.util.concurrent.ExecutorService; @@ -120,28 +118,10 @@ public class SystemDataTransferProcessor { } /** - * Resolve the requested association, throwing if the caller doesn't have - * adequate permissions. - */ - @NonNull - private AssociationInfo resolveAssociation(String packageName, int userId, - int associationId) { - AssociationInfo association = mAssociationStore.getAssociationById(associationId); - association = PermissionsUtils.sanitizeWithCallerChecks(mContext, association); - if (association == null) { - throw new DeviceNotAssociatedException("Association " - + associationId + " is not associated with the app " + packageName - + " for user " + userId); - } - return association; - } - - /** * Return whether the user has consented to the permission transfer for the association. */ - public boolean isPermissionTransferUserConsented(String packageName, @UserIdInt int userId, - int associationId) { - resolveAssociation(packageName, userId, associationId); + public boolean isPermissionTransferUserConsented(int associationId) { + mAssociationStore.getAssociationWithCallerChecks(associationId); PermissionSyncRequest request = getPermissionSyncRequest(associationId); if (request == null) { @@ -167,7 +147,8 @@ public class SystemDataTransferProcessor { return null; } - final AssociationInfo association = resolveAssociation(packageName, userId, associationId); + final AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); Slog.i(LOG_TAG, "Creating permission sync intent for userId [" + userId + "] associationId [" + associationId + "]"); @@ -207,7 +188,7 @@ public class SystemDataTransferProcessor { Slog.i(LOG_TAG, "Start system data transfer for package [" + packageName + "] userId [" + userId + "] associationId [" + associationId + "]"); - final AssociationInfo association = resolveAssociation(packageName, userId, associationId); + mAssociationStore.getAssociationWithCallerChecks(associationId); // Check if the request has been consented by the user. PermissionSyncRequest request = getPermissionSyncRequest(associationId); @@ -239,24 +220,20 @@ public class SystemDataTransferProcessor { * Enable perm sync for the association */ public void enablePermissionsSync(int associationId) { - Binder.withCleanCallingIdentity(() -> { - int userId = mAssociationStore.getAssociationById(associationId).getUserId(); - PermissionSyncRequest request = new PermissionSyncRequest(associationId); - request.setUserConsented(true); - mSystemDataTransferRequestStore.writeRequest(userId, request); - }); + int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId(); + PermissionSyncRequest request = new PermissionSyncRequest(associationId); + request.setUserConsented(true); + mSystemDataTransferRequestStore.writeRequest(userId, request); } /** * Disable perm sync for the association */ public void disablePermissionsSync(int associationId) { - Binder.withCleanCallingIdentity(() -> { - int userId = mAssociationStore.getAssociationById(associationId).getUserId(); - PermissionSyncRequest request = new PermissionSyncRequest(associationId); - request.setUserConsented(false); - mSystemDataTransferRequestStore.writeRequest(userId, request); - }); + int userId = mAssociationStore.getAssociationWithCallerChecks(associationId).getUserId(); + PermissionSyncRequest request = new PermissionSyncRequest(associationId); + request.setUserConsented(false); + mSystemDataTransferRequestStore.writeRequest(userId, request); } /** @@ -264,18 +241,17 @@ public class SystemDataTransferProcessor { */ @Nullable public PermissionSyncRequest getPermissionSyncRequest(int associationId) { - return Binder.withCleanCallingIdentity(() -> { - int userId = mAssociationStore.getAssociationById(associationId).getUserId(); - List<SystemDataTransferRequest> requests = - mSystemDataTransferRequestStore.readRequestsByAssociationId(userId, - associationId); - for (SystemDataTransferRequest request : requests) { - if (request instanceof PermissionSyncRequest) { - return (PermissionSyncRequest) request; - } + int userId = mAssociationStore.getAssociationWithCallerChecks(associationId) + .getUserId(); + List<SystemDataTransferRequest> requests = + mSystemDataTransferRequestStore.readRequestsByAssociationId(userId, + associationId); + for (SystemDataTransferRequest request : requests) { + if (request instanceof PermissionSyncRequest) { + return (PermissionSyncRequest) request; } - return null; - }); + } + return null; } /** diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java index c89ce11c169d..9c37881499bd 100644 --- a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java +++ b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java @@ -33,7 +33,7 @@ import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_FIRST_MATCH; import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_MATCH_LOST; import static android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_POWER; -import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG; +import static com.android.server.companion.presence.DevicePresenceProcessor.DEBUG; import static com.android.server.companion.utils.Utils.btDeviceToString; import static java.util.Objects.requireNonNull; diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java index cb363a7c9d7f..2d345c48a8eb 100644 --- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java +++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java @@ -19,7 +19,7 @@ package com.android.server.companion.presence; import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; -import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG; +import static com.android.server.companion.presence.DevicePresenceProcessor.DEBUG; import static com.android.server.companion.utils.Utils.btDeviceToString; import android.annotation.NonNull; diff --git a/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java b/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java new file mode 100644 index 000000000000..4ba4e2ce6899 --- /dev/null +++ b/services/companion/java/com/android/server/companion/presence/CompanionAppBinder.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.companion.presence; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.UserIdInt; +import android.companion.AssociationInfo; +import android.companion.CompanionDeviceService; +import android.companion.DevicePresenceEvent; +import android.content.ComponentName; +import android.content.Context; +import android.os.Handler; +import android.os.PowerManagerInternal; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.infra.PerUser; +import com.android.server.companion.CompanionDeviceManagerService; +import com.android.server.companion.association.AssociationStore; +import com.android.server.companion.utils.PackageUtils; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages communication with companion applications via + * {@link android.companion.ICompanionDeviceService} interface, including "connecting" (binding) to + * the services, maintaining the connection (the binding), and invoking callback methods such as + * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)}, + * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the + * application process. + * + * <p> + * The following is the list of the APIs provided by {@link CompanionAppBinder} (to be + * utilized by {@link CompanionDeviceManagerService}): + * <ul> + * <li> {@link #bindCompanionApplication(int, String, boolean, CompanionServiceConnector.Listener)} + * <li> {@link #unbindCompanionApplication(int, String)} + * <li> {@link #isCompanionApplicationBound(int, String)} + * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)} + * </ul> + * + * @see CompanionDeviceService + * @see android.companion.ICompanionDeviceService + * @see CompanionServiceConnector + */ +@SuppressLint("LongLogTag") +public class CompanionAppBinder { + private static final String TAG = "CDM_CompanionAppBinder"; + + private static final long REBIND_TIMEOUT = 10 * 1000; // 10 sec + + @NonNull + private final Context mContext; + @NonNull + private final AssociationStore mAssociationStore; + @NonNull + private final ObservableUuidStore mObservableUuidStore; + @NonNull + private final CompanionServicesRegister mCompanionServicesRegister; + + private final PowerManagerInternal mPowerManagerInternal; + + @NonNull + @GuardedBy("mBoundCompanionApplications") + private final AndroidPackageMap<List<CompanionServiceConnector>> + mBoundCompanionApplications; + @NonNull + @GuardedBy("mScheduledForRebindingCompanionApplications") + private final AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications; + + public CompanionAppBinder(@NonNull Context context, + @NonNull AssociationStore associationStore, + @NonNull ObservableUuidStore observableUuidStore, + @NonNull PowerManagerInternal powerManagerInternal) { + mContext = context; + mAssociationStore = associationStore; + mObservableUuidStore = observableUuidStore; + mPowerManagerInternal = powerManagerInternal; + mCompanionServicesRegister = new CompanionServicesRegister(); + mBoundCompanionApplications = new AndroidPackageMap<>(); + mScheduledForRebindingCompanionApplications = new AndroidPackageMap<>(); + } + + /** + * On package changed. + */ + public void onPackagesChanged(@UserIdInt int userId) { + mCompanionServicesRegister.invalidate(userId); + } + + /** + * CDM binds to the companion app. + */ + public void bindCompanionApplication(@UserIdInt int userId, @NonNull String packageName, + boolean isSelfManaged, CompanionServiceConnector.Listener listener) { + Slog.i(TAG, "Binding user=[" + userId + "], package=[" + packageName + "], isSelfManaged=[" + + isSelfManaged + "]..."); + + final List<ComponentName> companionServices = + mCompanionServicesRegister.forPackage(userId, packageName); + if (companionServices.isEmpty()) { + Slog.e(TAG, "Can not bind companion applications u" + userId + "/" + packageName + ": " + + "eligible CompanionDeviceService not found.\n" + + "A CompanionDeviceService should declare an intent-filter for " + + "\"android.companion.CompanionDeviceService\" action and require " + + "\"android.permission.BIND_COMPANION_DEVICE_SERVICE\" permission."); + return; + } + + final List<CompanionServiceConnector> serviceConnectors = new ArrayList<>(); + synchronized (mBoundCompanionApplications) { + if (mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { + Slog.w(TAG, "The package is ALREADY bound."); + return; + } + + for (int i = 0; i < companionServices.size(); i++) { + boolean isPrimary = i == 0; + serviceConnectors.add(CompanionServiceConnector.newInstance(mContext, userId, + companionServices.get(i), isSelfManaged, isPrimary)); + } + + mBoundCompanionApplications.setValueForPackage(userId, packageName, serviceConnectors); + } + + // Set listeners for both Primary and Secondary connectors. + for (CompanionServiceConnector serviceConnector : serviceConnectors) { + serviceConnector.setListener(listener); + } + + // Now "bind" all the connectors: the primary one and the rest of them. + for (CompanionServiceConnector serviceConnector : serviceConnectors) { + serviceConnector.connect(); + } + } + + /** + * CDM unbinds the companion app. + */ + public void unbindCompanionApplication(@UserIdInt int userId, @NonNull String packageName) { + Slog.i(TAG, "Unbinding user=[" + userId + "], package=[" + packageName + "]..."); + + final List<CompanionServiceConnector> serviceConnectors; + + synchronized (mBoundCompanionApplications) { + serviceConnectors = mBoundCompanionApplications.removePackage(userId, packageName); + } + + synchronized (mScheduledForRebindingCompanionApplications) { + mScheduledForRebindingCompanionApplications.removePackage(userId, packageName); + } + + if (serviceConnectors == null) { + Slog.e(TAG, "The package is not bound."); + return; + } + + for (CompanionServiceConnector serviceConnector : serviceConnectors) { + serviceConnector.postUnbind(); + } + } + + /** + * @return whether the companion application is bound now. + */ + public boolean isCompanionApplicationBound(@UserIdInt int userId, @NonNull String packageName) { + synchronized (mBoundCompanionApplications) { + return mBoundCompanionApplications.containsValueForPackage(userId, packageName); + } + } + + /** + * Remove bound apps for package. + */ + public void removePackage(int userId, String packageName) { + synchronized (mBoundCompanionApplications) { + mBoundCompanionApplications.removePackage(userId, packageName); + } + } + + /** + * Schedule rebinding for the package. + */ + public void scheduleRebinding(@UserIdInt int userId, @NonNull String packageName, + CompanionServiceConnector serviceConnector) { + Slog.i(TAG, "scheduleRebinding() " + userId + "/" + packageName); + + if (isRebindingCompanionApplicationScheduled(userId, packageName)) { + Slog.i(TAG, "CompanionApplication rebinding has been scheduled, skipping " + + serviceConnector.getComponentName()); + return; + } + + if (serviceConnector.isPrimary()) { + synchronized (mScheduledForRebindingCompanionApplications) { + mScheduledForRebindingCompanionApplications.setValueForPackage( + userId, packageName, true); + } + } + + // Rebinding in 10 seconds. + Handler.getMain().postDelayed(() -> + onRebindingCompanionApplicationTimeout(userId, packageName, + serviceConnector), + REBIND_TIMEOUT); + } + + private boolean isRebindingCompanionApplicationScheduled( + @UserIdInt int userId, @NonNull String packageName) { + synchronized (mScheduledForRebindingCompanionApplications) { + return mScheduledForRebindingCompanionApplications.containsValueForPackage( + userId, packageName); + } + } + + private void onRebindingCompanionApplicationTimeout( + @UserIdInt int userId, @NonNull String packageName, + @NonNull CompanionServiceConnector serviceConnector) { + // Re-mark the application is bound. + if (serviceConnector.isPrimary()) { + synchronized (mBoundCompanionApplications) { + if (!mBoundCompanionApplications.containsValueForPackage(userId, packageName)) { + List<CompanionServiceConnector> serviceConnectors = + Collections.singletonList(serviceConnector); + mBoundCompanionApplications.setValueForPackage(userId, packageName, + serviceConnectors); + } + } + + synchronized (mScheduledForRebindingCompanionApplications) { + mScheduledForRebindingCompanionApplications.removePackage(userId, packageName); + } + } + + serviceConnector.connect(); + } + + /** + * Dump bound apps. + */ + public void dump(@NonNull PrintWriter out) { + out.append("Companion Device Application Controller: \n"); + + synchronized (mBoundCompanionApplications) { + out.append(" Bound Companion Applications: "); + if (mBoundCompanionApplications.size() == 0) { + out.append("<empty>\n"); + } else { + out.append("\n"); + mBoundCompanionApplications.dump(out); + } + } + + out.append(" Companion Applications Scheduled For Rebinding: "); + synchronized (mScheduledForRebindingCompanionApplications) { + if (mScheduledForRebindingCompanionApplications.size() == 0) { + out.append("<empty>\n"); + } else { + out.append("\n"); + mScheduledForRebindingCompanionApplications.dump(out); + } + } + } + + @Nullable + CompanionServiceConnector getPrimaryServiceConnector( + @UserIdInt int userId, @NonNull String packageName) { + final List<CompanionServiceConnector> connectors; + synchronized (mBoundCompanionApplications) { + connectors = mBoundCompanionApplications.getValueForPackage(userId, packageName); + } + return connectors != null ? connectors.get(0) : null; + } + + private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> { + @Override + public synchronized @NonNull Map<String, List<ComponentName>> forUser( + @UserIdInt int userId) { + return super.forUser(userId); + } + + synchronized @NonNull List<ComponentName> forPackage( + @UserIdInt int userId, @NonNull String packageName) { + return forUser(userId).getOrDefault(packageName, Collections.emptyList()); + } + + synchronized void invalidate(@UserIdInt int userId) { + remove(userId); + } + + @Override + protected final @NonNull Map<String, List<ComponentName>> create(@UserIdInt int userId) { + return PackageUtils.getCompanionServicesForUser(mContext, userId); + } + } + + /** + * Associates an Android package (defined by userId + packageName) with a value of type T. + */ + private static class AndroidPackageMap<T> extends SparseArray<Map<String, T>> { + + void setValueForPackage( + @UserIdInt int userId, @NonNull String packageName, @NonNull T value) { + Map<String, T> forUser = get(userId); + if (forUser == null) { + forUser = /* Map<String, T> */ new HashMap(); + put(userId, forUser); + } + + forUser.put(packageName, value); + } + + boolean containsValueForPackage(@UserIdInt int userId, @NonNull String packageName) { + final Map<String, ?> forUser = get(userId); + return forUser != null && forUser.containsKey(packageName); + } + + T getValueForPackage(@UserIdInt int userId, @NonNull String packageName) { + final Map<String, T> forUser = get(userId); + return forUser != null ? forUser.get(packageName) : null; + } + + T removePackage(@UserIdInt int userId, @NonNull String packageName) { + final Map<String, T> forUser = get(userId); + if (forUser == null) return null; + return forUser.remove(packageName); + } + + void dump() { + if (size() == 0) { + Log.d(TAG, "<empty>"); + return; + } + + for (int i = 0; i < size(); i++) { + final int userId = keyAt(i); + final Map<String, T> forUser = get(userId); + if (forUser.isEmpty()) { + Log.d(TAG, "u" + userId + ": <empty>"); + } + + for (Map.Entry<String, T> packageValue : forUser.entrySet()) { + final String packageName = packageValue.getKey(); + final T value = packageValue.getValue(); + Log.d(TAG, "u" + userId + "\\" + packageName + " -> " + value); + } + } + } + + private void dump(@NonNull PrintWriter out) { + for (int i = 0; i < size(); i++) { + final int userId = keyAt(i); + final Map<String, T> forUser = get(userId); + if (forUser.isEmpty()) { + out.append(" u").append(String.valueOf(userId)).append(": <empty>\n"); + } + + for (Map.Entry<String, T> packageValue : forUser.entrySet()) { + final String packageName = packageValue.getKey(); + final T value = packageValue.getValue(); + out.append(" u").append(String.valueOf(userId)).append("\\") + .append(packageName).append(" -> ") + .append(value.toString()).append('\n'); + } + } + } + } +} diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java deleted file mode 100644 index 7a1a83f53315..000000000000 --- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java +++ /dev/null @@ -1,620 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.companion.presence; - -import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; -import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; -import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; -import static android.os.Process.ROOT_UID; -import static android.os.Process.SHELL_UID; - -import android.annotation.NonNull; -import android.annotation.SuppressLint; -import android.annotation.TestApi; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.companion.AssociationInfo; -import android.content.Context; -import android.os.Binder; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.ParcelUuid; -import android.os.UserManager; -import android.util.Log; -import android.util.Slog; -import android.util.SparseArray; -import android.util.SparseBooleanArray; - -import com.android.internal.annotations.GuardedBy; -import com.android.server.companion.association.AssociationStore; - -import java.io.PrintWriter; -import java.util.HashSet; -import java.util.Set; - -/** - * Class responsible for monitoring companion devices' "presence" status (i.e. - * connected/disconnected for Bluetooth devices; nearby or not for BLE devices). - * - * <p> - * Should only be used by - * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} - * to which it provides the following API: - * <ul> - * <li> {@link #onSelfManagedDeviceConnected(int)} - * <li> {@link #onSelfManagedDeviceDisconnected(int)} - * <li> {@link #isDevicePresent(int)} - * <li> {@link Callback#onDeviceAppeared(int) Callback.onDeviceAppeared(int)} - * <li> {@link Callback#onDeviceDisappeared(int) Callback.onDeviceDisappeared(int)} - * <li> {@link Callback#onDevicePresenceEvent(int, int)}} - * </ul> - */ -@SuppressLint("LongLogTag") -public class CompanionDevicePresenceMonitor implements AssociationStore.OnChangeListener, - BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback { - static final boolean DEBUG = false; - private static final String TAG = "CDM_CompanionDevicePresenceMonitor"; - - /** Callback for notifying about changes to status of companion devices. */ - public interface Callback { - /** Invoked when companion device is found nearby or connects. */ - void onDeviceAppeared(int associationId); - - /** Invoked when a companion device no longer seen nearby or disconnects. */ - void onDeviceDisappeared(int associationId); - - /** Invoked when device has corresponding event changes. */ - void onDevicePresenceEvent(int associationId, int event); - - /** Invoked when device has corresponding event changes base on the UUID */ - void onDevicePresenceEventByUuid(ObservableUuid uuid, int event); - } - - private final @NonNull AssociationStore mAssociationStore; - private final @NonNull ObservableUuidStore mObservableUuidStore; - private final @NonNull Callback mCallback; - private final @NonNull BluetoothCompanionDeviceConnectionListener mBtConnectionListener; - private final @NonNull BleCompanionDeviceScanner mBleScanner; - - // NOTE: Same association may appear in more than one of the following sets at the same time. - // (E.g. self-managed devices that have MAC addresses, could be reported as present by their - // companion applications, while at the same be connected via BT, or detected nearby by BLE - // scanner) - private final @NonNull Set<Integer> mConnectedBtDevices = new HashSet<>(); - private final @NonNull Set<Integer> mNearbyBleDevices = new HashSet<>(); - private final @NonNull Set<Integer> mReportedSelfManagedDevices = new HashSet<>(); - private final @NonNull Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>(); - @GuardedBy("mBtDisconnectedDevices") - private final @NonNull Set<Integer> mBtDisconnectedDevices = new HashSet<>(); - - // A map to track device presence within 10 seconds of Bluetooth disconnection. - // The key is the association ID, and the boolean value indicates if the device - // was detected again within that time frame. - @GuardedBy("mBtDisconnectedDevices") - private final @NonNull SparseBooleanArray mBtDisconnectedDevicesBlePresence = - new SparseBooleanArray(); - - // Tracking "simulated" presence. Used for debugging and testing only. - private final @NonNull Set<Integer> mSimulated = new HashSet<>(); - private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper = - new SimulatedDevicePresenceSchedulerHelper(); - - private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler = - new BleDeviceDisappearedScheduler(); - - public CompanionDevicePresenceMonitor(UserManager userManager, - @NonNull AssociationStore associationStore, - @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) { - mAssociationStore = associationStore; - mObservableUuidStore = observableUuidStore; - mCallback = callback; - mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager, - associationStore, mObservableUuidStore, - /* BluetoothCompanionDeviceConnectionListener.Callback */ this); - mBleScanner = new BleCompanionDeviceScanner(associationStore, - /* BleCompanionDeviceScanner.Callback */ this); - } - - /** Initialize {@link CompanionDevicePresenceMonitor} */ - public void init(Context context) { - if (DEBUG) Log.i(TAG, "init()"); - - final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); - if (btAdapter != null) { - mBtConnectionListener.init(btAdapter); - mBleScanner.init(context, btAdapter); - } else { - Log.w(TAG, "BluetoothAdapter is NOT available."); - } - - mAssociationStore.registerLocalListener(this); - } - - /** - * @return current connected UUID devices. - */ - public Set<ParcelUuid> getCurrentConnectedUuidDevices() { - return mConnectedUuidDevices; - } - - /** - * Remove current connected UUID device. - */ - public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) { - mConnectedUuidDevices.remove(uuid); - } - - /** - * @return whether the associated companion devices is present. I.e. device is nearby (for BLE); - * or devices is connected (for Bluetooth); or reported (by the application) to be - * nearby (for "self-managed" associations). - */ - public boolean isDevicePresent(int associationId) { - return mReportedSelfManagedDevices.contains(associationId) - || mConnectedBtDevices.contains(associationId) - || mNearbyBleDevices.contains(associationId) - || mSimulated.contains(associationId); - } - - /** - * @return whether the current uuid to be observed is present. - */ - public boolean isDeviceUuidPresent(ParcelUuid uuid) { - return mConnectedUuidDevices.contains(uuid); - } - - /** - * @return whether the current device is BT connected and had already reported to the app. - */ - - public boolean isBtConnected(int associationId) { - return mConnectedBtDevices.contains(associationId); - } - - /** - * @return whether the current device in BLE range and had already reported to the app. - */ - public boolean isBlePresent(int associationId) { - return mNearbyBleDevices.contains(associationId); - } - - /** - * @return whether the current device had been already reported by the simulator. - */ - public boolean isSimulatePresent(int associationId) { - return mSimulated.contains(associationId); - } - - /** - * Marks a "self-managed" device as connected. - * - * <p> - * Must ONLY be invoked by the - * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} - * when an application invokes - * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) notifyDeviceAppeared()} - */ - public void onSelfManagedDeviceConnected(int associationId) { - onDevicePresenceEvent(mReportedSelfManagedDevices, - associationId, EVENT_SELF_MANAGED_APPEARED); - } - - /** - * Marks a "self-managed" device as disconnected. - * - * <p> - * Must ONLY be invoked by the - * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} - * when an application invokes - * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) notifyDeviceDisappeared()} - */ - public void onSelfManagedDeviceDisconnected(int associationId) { - onDevicePresenceEvent(mReportedSelfManagedDevices, - associationId, EVENT_SELF_MANAGED_DISAPPEARED); - } - - /** - * Marks a "self-managed" device as disconnected when binderDied. - */ - public void onSelfManagedDeviceReporterBinderDied(int associationId) { - onDevicePresenceEvent(mReportedSelfManagedDevices, - associationId, EVENT_SELF_MANAGED_DISAPPEARED); - } - - @Override - public void onBluetoothCompanionDeviceConnected(int associationId) { - synchronized (mBtDisconnectedDevices) { - // A device is considered reconnected within 10 seconds if a pending BLE lost report is - // followed by a detected Bluetooth connection. - boolean isReconnected = mBtDisconnectedDevices.contains(associationId); - if (isReconnected) { - Slog.i(TAG, "Device ( " + associationId + " ) is reconnected within 10s."); - mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); - } - - Slog.i(TAG, "onBluetoothCompanionDeviceConnected: " - + "associationId( " + associationId + " )"); - onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED); - - // Stop the BLE scan if all devices report BT connected status and BLE was present. - if (canStopBleScan()) { - mBleScanner.stopScanIfNeeded(); - } - - } - } - - @Override - public void onBluetoothCompanionDeviceDisconnected(int associationId) { - Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected " - + "associationId( " + associationId + " )"); - // Start BLE scanning when the device is disconnected. - mBleScanner.startScan(); - - onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED); - // If current device is BLE present but BT is disconnected , means it will be - // potentially out of range later. Schedule BLE disappeared callback. - if (isBlePresent(associationId)) { - synchronized (mBtDisconnectedDevices) { - mBtDisconnectedDevices.add(associationId); - } - mBleDeviceDisappearedScheduler.scheduleBleDeviceDisappeared(associationId); - } - } - - @Override - public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) { - final ParcelUuid parcelUuid = uuid.getUuid(); - - switch(event) { - case EVENT_BT_CONNECTED: - boolean added = mConnectedUuidDevices.add(parcelUuid); - - if (!added) { - Slog.w(TAG, "Uuid= " + parcelUuid + "is ALREADY reported as " - + "present by this event=" + event); - } - - break; - case EVENT_BT_DISCONNECTED: - final boolean removed = mConnectedUuidDevices.remove(parcelUuid); - - if (!removed) { - Slog.w(TAG, "UUID= " + parcelUuid + " was NOT reported " - + "as present by this event= " + event); - - return; - } - - break; - } - - mCallback.onDevicePresenceEventByUuid(uuid, event); - } - - - @Override - public void onBleCompanionDeviceFound(int associationId) { - onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED); - synchronized (mBtDisconnectedDevices) { - final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId); - if (mBtDisconnectedDevices.contains(associationId) && isCurrentPresent) { - mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); - } - } - } - - @Override - public void onBleCompanionDeviceLost(int associationId) { - onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); - } - - /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ - @TestApi - public void simulateDeviceEvent(int associationId, int event) { - // IMPORTANT: this API should only be invoked via the - // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to - // make this call are SHELL and ROOT. - // No other caller (including SYSTEM!) should be allowed. - enforceCallerShellOrRoot(); - // Make sure the association exists. - enforceAssociationExists(associationId); - - switch (event) { - case EVENT_BLE_APPEARED: - simulateDeviceAppeared(associationId, event); - break; - case EVENT_BT_CONNECTED: - onBluetoothCompanionDeviceConnected(associationId); - break; - case EVENT_BLE_DISAPPEARED: - simulateDeviceDisappeared(associationId, event); - break; - case EVENT_BT_DISCONNECTED: - onBluetoothCompanionDeviceDisconnected(associationId); - break; - default: - throw new IllegalArgumentException("Event: " + event + "is not supported"); - } - } - - /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ - @TestApi - public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) { - // IMPORTANT: this API should only be invoked via the - // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to - // make this call are SHELL and ROOT. - // No other caller (including SYSTEM!) should be allowed. - enforceCallerShellOrRoot(); - onDevicePresenceEventByUuid(uuid, event); - } - - private void simulateDeviceAppeared(int associationId, int state) { - onDevicePresenceEvent(mSimulated, associationId, state); - mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); - } - - private void simulateDeviceDisappeared(int associationId, int state) { - mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); - onDevicePresenceEvent(mSimulated, associationId, state); - } - - private void enforceAssociationExists(int associationId) { - if (mAssociationStore.getAssociationById(associationId) == null) { - throw new IllegalArgumentException( - "Association with id " + associationId + " does not exist."); - } - } - - private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource, - int associationId, int event) { - Slog.i(TAG, "onDevicePresenceEvent() id=" + associationId + ", event=" + event); - - switch (event) { - case EVENT_BLE_APPEARED: - synchronized (mBtDisconnectedDevices) { - // If a BLE device is detected within 10 seconds after BT is disconnected, - // flag it as BLE is present. - if (mBtDisconnectedDevices.contains(associationId)) { - Slog.i(TAG, "Device ( " + associationId + " ) is present," - + " do not need to send the callback with event ( " - + EVENT_BLE_APPEARED + " )."); - mBtDisconnectedDevicesBlePresence.append(associationId, true); - } - } - case EVENT_BT_CONNECTED: - case EVENT_SELF_MANAGED_APPEARED: - final boolean added = presentDevicesForSource.add(associationId); - - if (!added) { - Slog.w(TAG, "Association with id " - + associationId + " is ALREADY reported as " - + "present by this source, event=" + event); - } - - mCallback.onDeviceAppeared(associationId); - - break; - case EVENT_BLE_DISAPPEARED: - case EVENT_BT_DISCONNECTED: - case EVENT_SELF_MANAGED_DISAPPEARED: - final boolean removed = presentDevicesForSource.remove(associationId); - - if (!removed) { - Slog.w(TAG, "Association with id " + associationId + " was NOT reported " - + "as present by this source, event= " + event); - - return; - } - - mCallback.onDeviceDisappeared(associationId); - - break; - default: - Slog.e(TAG, "Event: " + event + " is not supported"); - return; - } - - mCallback.onDevicePresenceEvent(associationId, event); - } - - /** - * Implements - * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)} - */ - @Override - public void onAssociationRemoved(@NonNull AssociationInfo association) { - final int id = association.getId(); - if (DEBUG) { - Log.i(TAG, "onAssociationRemoved() id=" + id); - Log.d(TAG, " > association=" + association); - } - - mConnectedBtDevices.remove(id); - mNearbyBleDevices.remove(id); - mReportedSelfManagedDevices.remove(id); - mSimulated.remove(id); - mBtDisconnectedDevices.remove(id); - mBtDisconnectedDevicesBlePresence.delete(id); - - // Do NOT call mCallback.onDeviceDisappeared()! - // CompanionDeviceManagerService will know that the association is removed, and will do - // what's needed. - } - - /** - * Return a set of devices that pending to report connectivity - */ - public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() { - synchronized (mBtConnectionListener.mPendingConnectedDevices) { - return mBtConnectionListener.mPendingConnectedDevices; - } - } - - private static void enforceCallerShellOrRoot() { - final int callingUid = Binder.getCallingUid(); - if (callingUid == SHELL_UID || callingUid == ROOT_UID) return; - - throw new SecurityException("Caller is neither Shell nor Root"); - } - - /** - * The BLE scan can be only stopped if all the devices have been reported - * BT connected and BLE presence and are not pending to report BLE lost. - */ - private boolean canStopBleScan() { - for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) { - int id = ai.getId(); - synchronized (mBtDisconnectedDevices) { - if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id) - && isBlePresent(id) && mBtDisconnectedDevices.isEmpty())) { - Slog.i(TAG, "The BLE scan cannot be stopped, " - + "device( " + id + " ) is not yet connected " - + "OR the BLE is not current present Or is pending to report BLE lost"); - return false; - } - } - } - return true; - } - - /** - * Dumps system information about devices that are marked as "present". - */ - public void dump(@NonNull PrintWriter out) { - out.append("Companion Device Present: "); - if (mConnectedBtDevices.isEmpty() - && mNearbyBleDevices.isEmpty() - && mReportedSelfManagedDevices.isEmpty()) { - out.append("<empty>\n"); - return; - } else { - out.append("\n"); - } - - out.append(" Connected Bluetooth Devices: "); - if (mConnectedBtDevices.isEmpty()) { - out.append("<empty>\n"); - } else { - out.append("\n"); - for (int associationId : mConnectedBtDevices) { - AssociationInfo a = mAssociationStore.getAssociationById(associationId); - out.append(" ").append(a.toShortString()).append('\n'); - } - } - - out.append(" Nearby BLE Devices: "); - if (mNearbyBleDevices.isEmpty()) { - out.append("<empty>\n"); - } else { - out.append("\n"); - for (int associationId : mNearbyBleDevices) { - AssociationInfo a = mAssociationStore.getAssociationById(associationId); - out.append(" ").append(a.toShortString()).append('\n'); - } - } - - out.append(" Self-Reported Devices: "); - if (mReportedSelfManagedDevices.isEmpty()) { - out.append("<empty>\n"); - } else { - out.append("\n"); - for (int associationId : mReportedSelfManagedDevices) { - AssociationInfo a = mAssociationStore.getAssociationById(associationId); - out.append(" ").append(a.toShortString()).append('\n'); - } - } - } - - private class SimulatedDevicePresenceSchedulerHelper extends Handler { - SimulatedDevicePresenceSchedulerHelper() { - super(Looper.getMainLooper()); - } - - void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { - // First, unschedule if it was scheduled previously. - if (hasMessages(/* what */ associationId)) { - removeMessages(/* what */ associationId); - } - - sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */); - } - - void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { - removeMessages(/* what */ associationId); - } - - @Override - public void handleMessage(@NonNull Message msg) { - final int associationId = msg.what; - if (mSimulated.contains(associationId)) { - onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED); - } - } - } - - private class BleDeviceDisappearedScheduler extends Handler { - BleDeviceDisappearedScheduler() { - super(Looper.getMainLooper()); - } - - void scheduleBleDeviceDisappeared(int associationId) { - if (hasMessages(associationId)) { - removeMessages(associationId); - } - Slog.i(TAG, "scheduleBleDeviceDisappeared for Device: ( " + associationId + " )."); - sendEmptyMessageDelayed(associationId, 10 * 1000 /* 10 seconds */); - } - - void unScheduleDeviceDisappeared(int associationId) { - if (hasMessages(associationId)) { - Slog.i(TAG, "unScheduleDeviceDisappeared for Device( " + associationId + " )"); - synchronized (mBtDisconnectedDevices) { - mBtDisconnectedDevices.remove(associationId); - mBtDisconnectedDevicesBlePresence.delete(associationId); - } - - removeMessages(associationId); - } - } - - @Override - public void handleMessage(@NonNull Message msg) { - final int associationId = msg.what; - synchronized (mBtDisconnectedDevices) { - final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get( - associationId); - // If a device hasn't reported after 10 seconds and is not currently present, - // assume BLE is lost and trigger the onDeviceEvent callback with the - // EVENT_BLE_DISAPPEARED event. - if (mBtDisconnectedDevices.contains(associationId) - && !isCurrentPresent) { - Slog.i(TAG, "Device ( " + associationId + " ) is likely BLE out of range, " - + "sending callback with event ( " + EVENT_BLE_DISAPPEARED + " )"); - onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); - } - - mBtDisconnectedDevices.remove(associationId); - mBtDisconnectedDevicesBlePresence.delete(associationId); - } - } - } -} diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java index 5abdb42b34fc..c01c3195e04d 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java +++ b/services/companion/java/com/android/server/companion/presence/CompanionServiceConnector.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server.companion; +package com.android.server.companion.presence; import static android.content.Context.BIND_ALMOST_PERCEPTIBLE; import static android.content.Context.BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE; @@ -33,36 +33,42 @@ import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; -import android.util.Log; +import android.util.Slog; import com.android.internal.infra.ServiceConnector; import com.android.server.ServiceThread; +import com.android.server.companion.CompanionDeviceManagerService; /** * Manages a connection (binding) to an instance of {@link CompanionDeviceService} running in the * application process. */ @SuppressLint("LongLogTag") -class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> { - private static final String TAG = "CDM_CompanionServiceConnector"; - private static final boolean DEBUG = false; +public class CompanionServiceConnector extends ServiceConnector.Impl<ICompanionDeviceService> { - /* Unbinding before executing the callbacks can cause problems. Wait 5-seconds before unbind. */ - private static final long UNBIND_POST_DELAY_MS = 5_000; - - /** Listener for changes to the state of the {@link CompanionDeviceServiceConnector} */ - interface Listener { + /** Listener for changes to the state of the {@link CompanionServiceConnector} */ + public interface Listener { + /** + * Called when service binding is died. + */ void onBindingDied(@UserIdInt int userId, @NonNull String packageName, - @NonNull CompanionDeviceServiceConnector serviceConnector); + @NonNull CompanionServiceConnector serviceConnector); } - private final @UserIdInt int mUserId; - private final @NonNull ComponentName mComponentName; + private static final String TAG = "CDM_CompanionServiceConnector"; + + /* Unbinding before executing the callbacks can cause problems. Wait 5-seconds before unbind. */ + private static final long UNBIND_POST_DELAY_MS = 5_000; + @UserIdInt + private final int mUserId; + @NonNull + private final ComponentName mComponentName; + private final boolean mIsPrimary; // IMPORTANT: this can (and will!) be null (at the moment, CompanionApplicationController only // installs a listener to the primary ServiceConnector), hence we should always null-check the // reference before calling on it. - private @Nullable Listener mListener; - private boolean mIsPrimary; + @Nullable + private Listener mListener; /** * Create a CompanionDeviceServiceConnector instance. @@ -79,16 +85,16 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe * IMPORTANCE_FOREGROUND_SERVICE = 125. In order to kill the one time permission session, the * service importance level should be higher than 125. */ - static CompanionDeviceServiceConnector newInstance(@NonNull Context context, + static CompanionServiceConnector newInstance(@NonNull Context context, @UserIdInt int userId, @NonNull ComponentName componentName, boolean isSelfManaged, boolean isPrimary) { final int bindingFlags = isSelfManaged ? BIND_TREAT_LIKE_VISIBLE_FOREGROUND_SERVICE : BIND_ALMOST_PERCEPTIBLE; - return new CompanionDeviceServiceConnector( + return new CompanionServiceConnector( context, userId, componentName, bindingFlags, isPrimary); } - private CompanionDeviceServiceConnector(@NonNull Context context, @UserIdInt int userId, + private CompanionServiceConnector(@NonNull Context context, @UserIdInt int userId, @NonNull ComponentName componentName, int bindingFlags, boolean isPrimary) { super(context, buildIntent(componentName), bindingFlags, userId, null); mUserId = userId; @@ -133,6 +139,7 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe return mIsPrimary; } + @NonNull ComponentName getComponentName() { return mComponentName; } @@ -140,17 +147,15 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe @Override protected void onServiceConnectionStatusChanged( @NonNull ICompanionDeviceService service, boolean isConnected) { - if (DEBUG) { - Log.d(TAG, "onServiceConnection_StatusChanged() " + mComponentName.toShortString() - + " connected=" + isConnected); - } + Slog.d(TAG, "onServiceConnectionStatusChanged() " + mComponentName.toShortString() + + " connected=" + isConnected); } @Override public void binderDied() { super.binderDied(); - if (DEBUG) Log.d(TAG, "binderDied() " + mComponentName.toShortString()); + Slog.d(TAG, "binderDied() " + mComponentName.toShortString()); // Handle primary process being killed if (mListener != null) { @@ -172,7 +177,8 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe * within system_server and thus tends to get heavily congested) */ @Override - protected @NonNull Handler getJobHandler() { + @NonNull + protected Handler getJobHandler() { return getServiceThread().getThreadHandler(); } @@ -182,12 +188,14 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe return -1; } - private static @NonNull Intent buildIntent(@NonNull ComponentName componentName) { + @NonNull + private static Intent buildIntent(@NonNull ComponentName componentName) { return new Intent(CompanionDeviceService.SERVICE_INTERFACE) .setComponent(componentName); } - private static @NonNull ServiceThread getServiceThread() { + @NonNull + private static ServiceThread getServiceThread() { if (sServiceThread == null) { synchronized (CompanionDeviceManagerService.class) { if (sServiceThread == null) { @@ -206,5 +214,6 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe * <p> * Do NOT reference directly, use {@link #getServiceThread()} method instead. */ - private static volatile @Nullable ServiceThread sServiceThread; + @Nullable + private static volatile ServiceThread sServiceThread; } diff --git a/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java new file mode 100644 index 000000000000..2a933a8340c6 --- /dev/null +++ b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java @@ -0,0 +1,1042 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.companion.presence; + +import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; +import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; +import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; +import static android.companion.DevicePresenceEvent.NO_ASSOCIATION; +import static android.os.Process.ROOT_UID; +import static android.os.Process.SHELL_UID; + +import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; +import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanObserveDevicePresenceByUuid; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.TestApi; +import android.annotation.UserIdInt; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.companion.AssociationInfo; +import android.companion.DeviceNotAssociatedException; +import android.companion.DevicePresenceEvent; +import android.companion.ObservingDevicePresenceRequest; +import android.content.Context; +import android.hardware.power.Mode; +import android.os.Binder; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelUuid; +import android.os.PowerManagerInternal; +import android.os.RemoteException; +import android.os.UserManager; +import android.util.Log; +import android.util.Slog; +import android.util.SparseArray; +import android.util.SparseBooleanArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.companion.association.AssociationStore; + +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class responsible for monitoring companion devices' "presence" status (i.e. + * connected/disconnected for Bluetooth devices; nearby or not for BLE devices). + * + * <p> + * Should only be used by + * {@link com.android.server.companion.CompanionDeviceManagerService CompanionDeviceManagerService} + * to which it provides the following API: + * <ul> + * <li> {@link #onSelfManagedDeviceConnected(int)} + * <li> {@link #onSelfManagedDeviceDisconnected(int)} + * <li> {@link #isDevicePresent(int)} + * </ul> + */ +@SuppressLint("LongLogTag") +public class DevicePresenceProcessor implements AssociationStore.OnChangeListener, + BluetoothCompanionDeviceConnectionListener.Callback, BleCompanionDeviceScanner.Callback { + static final boolean DEBUG = false; + private static final String TAG = "CDM_DevicePresenceProcessor"; + + @NonNull + private final Context mContext; + @NonNull + private final CompanionAppBinder mCompanionAppBinder; + @NonNull + private final AssociationStore mAssociationStore; + @NonNull + private final ObservableUuidStore mObservableUuidStore; + @NonNull + private final BluetoothCompanionDeviceConnectionListener mBtConnectionListener; + @NonNull + private final BleCompanionDeviceScanner mBleScanner; + @NonNull + private final PowerManagerInternal mPowerManagerInternal; + + // NOTE: Same association may appear in more than one of the following sets at the same time. + // (E.g. self-managed devices that have MAC addresses, could be reported as present by their + // companion applications, while at the same be connected via BT, or detected nearby by BLE + // scanner) + @NonNull + private final Set<Integer> mConnectedBtDevices = new HashSet<>(); + @NonNull + private final Set<Integer> mNearbyBleDevices = new HashSet<>(); + @NonNull + private final Set<Integer> mReportedSelfManagedDevices = new HashSet<>(); + @NonNull + private final Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>(); + @NonNull + @GuardedBy("mBtDisconnectedDevices") + private final Set<Integer> mBtDisconnectedDevices = new HashSet<>(); + + // A map to track device presence within 10 seconds of Bluetooth disconnection. + // The key is the association ID, and the boolean value indicates if the device + // was detected again within that time frame. + @GuardedBy("mBtDisconnectedDevices") + private final @NonNull SparseBooleanArray mBtDisconnectedDevicesBlePresence = + new SparseBooleanArray(); + + // Tracking "simulated" presence. Used for debugging and testing only. + private final @NonNull Set<Integer> mSimulated = new HashSet<>(); + private final SimulatedDevicePresenceSchedulerHelper mSchedulerHelper = + new SimulatedDevicePresenceSchedulerHelper(); + + private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler = + new BleDeviceDisappearedScheduler(); + + public DevicePresenceProcessor(@NonNull Context context, + @NonNull CompanionAppBinder companionAppBinder, + UserManager userManager, + @NonNull AssociationStore associationStore, + @NonNull ObservableUuidStore observableUuidStore, + @NonNull PowerManagerInternal powerManagerInternal) { + mContext = context; + mCompanionAppBinder = companionAppBinder; + mAssociationStore = associationStore; + mObservableUuidStore = observableUuidStore; + mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager, + associationStore, mObservableUuidStore, + /* BluetoothCompanionDeviceConnectionListener.Callback */ this); + mBleScanner = new BleCompanionDeviceScanner(associationStore, + /* BleCompanionDeviceScanner.Callback */ this); + mPowerManagerInternal = powerManagerInternal; + } + + /** Initialize {@link DevicePresenceProcessor} */ + public void init(Context context) { + if (DEBUG) Slog.i(TAG, "init()"); + + final BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); + if (btAdapter != null) { + mBtConnectionListener.init(btAdapter); + mBleScanner.init(context, btAdapter); + } else { + Slog.w(TAG, "BluetoothAdapter is NOT available."); + } + + mAssociationStore.registerLocalListener(this); + } + + /** + * Process device presence start request. + */ + public void startObservingDevicePresence(ObservingDevicePresenceRequest request, + String packageName, int userId) { + Slog.i(TAG, + "Start observing request=[" + request + "] for userId=[" + userId + "], package=[" + + packageName + "]..."); + final ParcelUuid requestUuid = request.getUuid(); + + if (requestUuid != null) { + enforceCallerCanObserveDevicePresenceByUuid(mContext); + + // If it's already being observed, then no-op. + if (mObservableUuidStore.isUuidBeingObserved(requestUuid, userId, packageName)) { + Slog.i(TAG, "UUID=[" + requestUuid + "], package=[" + packageName + "], userId=[" + + userId + "] is already being observed."); + return; + } + + final ObservableUuid observableUuid = new ObservableUuid(userId, requestUuid, + packageName, System.currentTimeMillis()); + mObservableUuidStore.writeObservableUuid(userId, observableUuid); + } else { + final int associationId = request.getAssociationId(); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + + // If it's already being observed, then no-op. + if (association.isNotifyOnDeviceNearby()) { + Slog.i(TAG, "Associated device id=[" + association.getId() + + "] is already being observed. No-op."); + return; + } + + association = (new AssociationInfo.Builder(association)).setNotifyOnDeviceNearby(true) + .build(); + mAssociationStore.updateAssociation(association); + + // Send callback immediately if the device is present. + if (isDevicePresent(associationId)) { + Slog.i(TAG, "Device is already present. Triggering callback."); + if (isBlePresent(associationId)) { + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED); + } else if (isBtConnected(associationId)) { + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED); + } else if (isSimulatePresent(associationId)) { + onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_APPEARED); + } + } + } + + Slog.i(TAG, "Registered device presence listener."); + } + + /** + * Process device presence stop request. + */ + public void stopObservingDevicePresence(ObservingDevicePresenceRequest request, + String packageName, int userId) { + Slog.i(TAG, + "Stop observing request=[" + request + "] for userId=[" + userId + "], package=[" + + packageName + "]..."); + + final ParcelUuid requestUuid = request.getUuid(); + + if (requestUuid != null) { + enforceCallerCanObserveDevicePresenceByUuid(mContext); + + if (!mObservableUuidStore.isUuidBeingObserved(requestUuid, userId, packageName)) { + Slog.i(TAG, "UUID=[" + requestUuid + "], package=[" + packageName + "], userId=[" + + userId + "] is already not being observed."); + return; + } + + mObservableUuidStore.removeObservableUuid(userId, requestUuid, packageName); + removeCurrentConnectedUuidDevice(requestUuid); + } else { + final int associationId = request.getAssociationId(); + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + + // If it's already being observed, then no-op. + if (!association.isNotifyOnDeviceNearby()) { + Slog.i(TAG, "Associated device id=[" + association.getId() + + "] is already not being observed. No-op."); + return; + } + + association = (new AssociationInfo.Builder(association)).setNotifyOnDeviceNearby(false) + .build(); + mAssociationStore.updateAssociation(association); + } + + Slog.i(TAG, "Unregistered device presence listener."); + + // If last listener is unregistered, then unbind application. + if (!shouldBindPackage(userId, packageName)) { + mCompanionAppBinder.unbindCompanionApplication(userId, packageName); + } + } + + /** + * For legacy device presence below Android V. + * + * @deprecated Use {@link #startObservingDevicePresence(ObservingDevicePresenceRequest, String, + * int)} + */ + @Deprecated + public void startObservingDevicePresence(int userId, String packageName, String deviceAddress) + throws RemoteException { + Slog.i(TAG, + "Start observing device=[" + deviceAddress + "] for userId=[" + userId + + "], package=[" + + packageName + "]..."); + + enforceCallerCanManageAssociationsForPackage(mContext, userId, packageName, null); + + AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId, + packageName, deviceAddress); + + if (association == null) { + throw new RemoteException(new DeviceNotAssociatedException("App " + packageName + + " is not associated with device " + deviceAddress + + " for user " + userId)); + } + + startObservingDevicePresence( + new ObservingDevicePresenceRequest.Builder().setAssociationId(association.getId()) + .build(), packageName, userId); + } + + /** + * For legacy device presence below Android V. + * + * @deprecated Use {@link #stopObservingDevicePresence(ObservingDevicePresenceRequest, String, + * int)} + */ + @Deprecated + public void stopObservingDevicePresence(int userId, String packageName, String deviceAddress) + throws RemoteException { + Slog.i(TAG, + "Stop observing device=[" + deviceAddress + "] for userId=[" + userId + + "], package=[" + + packageName + "]..."); + + enforceCallerCanManageAssociationsForPackage(mContext, userId, packageName, null); + + AssociationInfo association = mAssociationStore.getFirstAssociationByAddress(userId, + packageName, deviceAddress); + + if (association == null) { + throw new RemoteException(new DeviceNotAssociatedException("App " + packageName + + " is not associated with device " + deviceAddress + + " for user " + userId)); + } + + stopObservingDevicePresence( + new ObservingDevicePresenceRequest.Builder().setAssociationId(association.getId()) + .build(), packageName, userId); + } + + /** + * @return whether the package should be bound (i.e. at least one of the devices associated with + * the package is currently present OR the UUID to be observed by this package is + * currently present). + */ + private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) { + final List<AssociationInfo> packageAssociations = + mAssociationStore.getActiveAssociationsByPackage(userId, packageName); + final List<ObservableUuid> observableUuids = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + + for (AssociationInfo association : packageAssociations) { + if (!association.shouldBindWhenPresent()) continue; + if (isDevicePresent(association.getId())) return true; + } + + for (ObservableUuid uuid : observableUuids) { + if (isDeviceUuidPresent(uuid.getUuid())) { + return true; + } + } + + return false; + } + + /** + * Bind the system to the app if it's not bound. + * + * Set bindImportant to true when the association is self-managed to avoid the target service + * being killed. + */ + private void bindApplicationIfNeeded(int userId, String packageName, boolean bindImportant) { + if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) { + mCompanionAppBinder.bindCompanionApplication( + userId, packageName, bindImportant, this::onBinderDied); + } else { + Slog.i(TAG, + "UserId=[" + userId + "], packageName=[" + packageName + "] is already bound."); + } + } + + /** + * @return current connected UUID devices. + */ + public Set<ParcelUuid> getCurrentConnectedUuidDevices() { + return mConnectedUuidDevices; + } + + /** + * Remove current connected UUID device. + */ + public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) { + mConnectedUuidDevices.remove(uuid); + } + + /** + * @return whether the associated companion devices is present. I.e. device is nearby (for BLE); + * or devices is connected (for Bluetooth); or reported (by the application) to be + * nearby (for "self-managed" associations). + */ + public boolean isDevicePresent(int associationId) { + return mReportedSelfManagedDevices.contains(associationId) + || mConnectedBtDevices.contains(associationId) + || mNearbyBleDevices.contains(associationId) + || mSimulated.contains(associationId); + } + + /** + * @return whether the current uuid to be observed is present. + */ + public boolean isDeviceUuidPresent(ParcelUuid uuid) { + return mConnectedUuidDevices.contains(uuid); + } + + /** + * @return whether the current device is BT connected and had already reported to the app. + */ + + public boolean isBtConnected(int associationId) { + return mConnectedBtDevices.contains(associationId); + } + + /** + * @return whether the current device in BLE range and had already reported to the app. + */ + public boolean isBlePresent(int associationId) { + return mNearbyBleDevices.contains(associationId); + } + + /** + * @return whether the current device had been already reported by the simulator. + */ + public boolean isSimulatePresent(int associationId) { + return mSimulated.contains(associationId); + } + + /** + * Marks a "self-managed" device as connected. + * + * <p> + * Must ONLY be invoked by the + * {@link com.android.server.companion.CompanionDeviceManagerService + * CompanionDeviceManagerService} + * when an application invokes + * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) + * notifyDeviceAppeared()} + */ + public void onSelfManagedDeviceConnected(int associationId) { + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_APPEARED); + } + + /** + * Marks a "self-managed" device as disconnected. + * + * <p> + * Must ONLY be invoked by the + * {@link com.android.server.companion.CompanionDeviceManagerService + * CompanionDeviceManagerService} + * when an application invokes + * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) + * notifyDeviceDisappeared()} + */ + public void onSelfManagedDeviceDisconnected(int associationId) { + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_DISAPPEARED); + } + + /** + * Marks a "self-managed" device as disconnected when binderDied. + */ + public void onSelfManagedDeviceReporterBinderDied(int associationId) { + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_DISAPPEARED); + } + + @Override + public void onBluetoothCompanionDeviceConnected(int associationId) { + synchronized (mBtDisconnectedDevices) { + // A device is considered reconnected within 10 seconds if a pending BLE lost report is + // followed by a detected Bluetooth connection. + boolean isReconnected = mBtDisconnectedDevices.contains(associationId); + if (isReconnected) { + Slog.i(TAG, "Device ( " + associationId + " ) is reconnected within 10s."); + mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); + } + + Slog.i(TAG, "onBluetoothCompanionDeviceConnected: " + + "associationId( " + associationId + " )"); + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED); + + // Stop the BLE scan if all devices report BT connected status and BLE was present. + if (canStopBleScan()) { + mBleScanner.stopScanIfNeeded(); + } + + } + } + + @Override + public void onBluetoothCompanionDeviceDisconnected(int associationId) { + Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected " + + "associationId( " + associationId + " )"); + // Start BLE scanning when the device is disconnected. + mBleScanner.startScan(); + + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED); + // If current device is BLE present but BT is disconnected , means it will be + // potentially out of range later. Schedule BLE disappeared callback. + if (isBlePresent(associationId)) { + synchronized (mBtDisconnectedDevices) { + mBtDisconnectedDevices.add(associationId); + } + mBleDeviceDisappearedScheduler.scheduleBleDeviceDisappeared(associationId); + } + } + + + @Override + public void onBleCompanionDeviceFound(int associationId) { + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED); + synchronized (mBtDisconnectedDevices) { + final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId); + if (mBtDisconnectedDevices.contains(associationId) && isCurrentPresent) { + mBleDeviceDisappearedScheduler.unScheduleDeviceDisappeared(associationId); + } + } + } + + @Override + public void onBleCompanionDeviceLost(int associationId) { + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); + } + + /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ + @TestApi + public void simulateDeviceEvent(int associationId, int event) { + // IMPORTANT: this API should only be invoked via the + // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to + // make this call are SHELL and ROOT. + // No other caller (including SYSTEM!) should be allowed. + enforceCallerShellOrRoot(); + // Make sure the association exists. + enforceAssociationExists(associationId); + + switch (event) { + case EVENT_BLE_APPEARED: + simulateDeviceAppeared(associationId, event); + break; + case EVENT_BT_CONNECTED: + onBluetoothCompanionDeviceConnected(associationId); + break; + case EVENT_BLE_DISAPPEARED: + simulateDeviceDisappeared(associationId, event); + break; + case EVENT_BT_DISCONNECTED: + onBluetoothCompanionDeviceDisconnected(associationId); + break; + default: + throw new IllegalArgumentException("Event: " + event + "is not supported"); + } + } + + /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ + @TestApi + public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) { + // IMPORTANT: this API should only be invoked via the + // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to + // make this call are SHELL and ROOT. + // No other caller (including SYSTEM!) should be allowed. + enforceCallerShellOrRoot(); + onDevicePresenceEventByUuid(uuid, event); + } + + private void simulateDeviceAppeared(int associationId, int state) { + onDevicePresenceEvent(mSimulated, associationId, state); + mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); + } + + private void simulateDeviceDisappeared(int associationId, int state) { + mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); + onDevicePresenceEvent(mSimulated, associationId, state); + } + + private void enforceAssociationExists(int associationId) { + if (mAssociationStore.getAssociationById(associationId) == null) { + throw new IllegalArgumentException( + "Association with id " + associationId + " does not exist."); + } + } + + private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource, + int associationId, int eventType) { + Slog.i(TAG, + "onDevicePresenceEvent() id=[" + associationId + "], event=[" + eventType + "]..."); + + AssociationInfo association = mAssociationStore.getAssociationById(associationId); + if (association == null) { + Slog.e(TAG, "Association doesn't exist."); + return; + } + + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + final DevicePresenceEvent event = new DevicePresenceEvent(associationId, eventType, null); + + if (eventType == EVENT_BLE_APPEARED) { + synchronized (mBtDisconnectedDevices) { + // If a BLE device is detected within 10 seconds after BT is disconnected, + // flag it as BLE is present. + if (mBtDisconnectedDevices.contains(associationId)) { + Slog.i(TAG, "Device ( " + associationId + " ) is present," + + " do not need to send the callback with event ( " + + EVENT_BLE_APPEARED + " )."); + mBtDisconnectedDevicesBlePresence.append(associationId, true); + } + } + } + + switch (eventType) { + case EVENT_BLE_APPEARED: + case EVENT_BT_CONNECTED: + case EVENT_SELF_MANAGED_APPEARED: + final boolean added = presentDevicesForSource.add(associationId); + if (!added) { + Slog.w(TAG, "The association is already present."); + } + + if (association.shouldBindWhenPresent()) { + bindApplicationIfNeeded(userId, packageName, association.isSelfManaged()); + } else { + return; + } + + if (association.isSelfManaged() || added) { + notifyDevicePresenceEvent(userId, packageName, event); + // Also send the legacy callback. + legacyNotifyDevicePresenceEvent(association, true); + } + break; + case EVENT_BLE_DISAPPEARED: + case EVENT_BT_DISCONNECTED: + case EVENT_SELF_MANAGED_DISAPPEARED: + final boolean removed = presentDevicesForSource.remove(associationId); + if (!removed) { + Slog.w(TAG, "The association is already NOT present."); + } + + if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) { + Slog.e(TAG, "Package is not bound"); + return; + } + + if (association.isSelfManaged() || removed) { + notifyDevicePresenceEvent(userId, packageName, event); + // Also send the legacy callback. + legacyNotifyDevicePresenceEvent(association, false); + } + + // Check if there are other devices associated to the app that are present. + if (!shouldBindPackage(userId, packageName)) { + mCompanionAppBinder.unbindCompanionApplication(userId, packageName); + } + break; + default: + Slog.e(TAG, "Event: " + eventType + " is not supported."); + break; + } + } + + @Override + public void onDevicePresenceEventByUuid(ObservableUuid uuid, int eventType) { + Slog.i(TAG, "onDevicePresenceEventByUuid ObservableUuid=[" + uuid + "], event=[" + eventType + + "]..."); + + final ParcelUuid parcelUuid = uuid.getUuid(); + final String packageName = uuid.getPackageName(); + final int userId = uuid.getUserId(); + final DevicePresenceEvent event = new DevicePresenceEvent(NO_ASSOCIATION, eventType, + parcelUuid); + + switch (eventType) { + case EVENT_BT_CONNECTED: + boolean added = mConnectedUuidDevices.add(parcelUuid); + if (!added) { + Slog.w(TAG, "This device is already connected."); + } + + bindApplicationIfNeeded(userId, packageName, false); + + notifyDevicePresenceEvent(userId, packageName, event); + break; + case EVENT_BT_DISCONNECTED: + final boolean removed = mConnectedUuidDevices.remove(parcelUuid); + if (!removed) { + Slog.w(TAG, "This device is already disconnected."); + return; + } + + if (!mCompanionAppBinder.isCompanionApplicationBound(userId, packageName)) { + Slog.e(TAG, "Package is not bound."); + return; + } + + notifyDevicePresenceEvent(userId, packageName, event); + + if (!shouldBindPackage(userId, packageName)) { + mCompanionAppBinder.unbindCompanionApplication(userId, packageName); + } + break; + default: + Slog.e(TAG, "Event: " + eventType + " is not supported"); + break; + } + } + + /** + * Notify device presence event to the app. + * + * @deprecated Use {@link #notifyDevicePresenceEvent(int, String, DevicePresenceEvent)} instead. + */ + @Deprecated + private void legacyNotifyDevicePresenceEvent(AssociationInfo association, + boolean isAppeared) { + Slog.i(TAG, "legacyNotifyDevicePresenceEvent() association=[" + association.toShortString() + + "], isAppeared=[" + isAppeared + "]"); + + final int userId = association.getUserId(); + final String packageName = association.getPackageName(); + + final CompanionServiceConnector primaryServiceConnector = + mCompanionAppBinder.getPrimaryServiceConnector(userId, packageName); + if (primaryServiceConnector == null) { + Slog.e(TAG, "Package is not bound."); + return; + } + + if (isAppeared) { + primaryServiceConnector.postOnDeviceAppeared(association); + } else { + primaryServiceConnector.postOnDeviceDisappeared(association); + } + } + + /** + * Notify the device presence event to the app. + */ + private void notifyDevicePresenceEvent(int userId, String packageName, + DevicePresenceEvent event) { + Slog.i(TAG, + "notifyCompanionDevicePresenceEvent userId=[" + userId + "], packageName=[" + + packageName + "], event=[" + event + "]..."); + + final CompanionServiceConnector primaryServiceConnector = + mCompanionAppBinder.getPrimaryServiceConnector(userId, packageName); + + if (primaryServiceConnector == null) { + Slog.e(TAG, "Package is NOT bound."); + return; + } + + primaryServiceConnector.postOnDevicePresenceEvent(event); + } + + /** + * Notify the self-managed device presence event to the app. + */ + public void notifySelfManagedDevicePresenceEvent(int associationId, boolean isAppeared) { + Slog.i(TAG, "notifySelfManagedDeviceAppeared() id=" + associationId); + + AssociationInfo association = mAssociationStore.getAssociationWithCallerChecks( + associationId); + if (!association.isSelfManaged()) { + throw new IllegalArgumentException("Association id=[" + associationId + + "] is not self-managed."); + } + // AssociationInfo class is immutable: create a new AssociationInfo object with updated + // timestamp. + association = (new AssociationInfo.Builder(association)) + .setLastTimeConnected(System.currentTimeMillis()) + .build(); + mAssociationStore.updateAssociation(association); + + if (isAppeared) { + onSelfManagedDeviceConnected(associationId); + } else { + onSelfManagedDeviceDisconnected(associationId); + } + + final String deviceProfile = association.getDeviceProfile(); + if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { + Slog.i(TAG, "Enable hint mode for device device profile: " + deviceProfile); + mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, isAppeared); + } + } + + private void onBinderDied(@UserIdInt int userId, @NonNull String packageName, + @NonNull CompanionServiceConnector serviceConnector) { + + boolean isPrimary = serviceConnector.isPrimary(); + Slog.i(TAG, "onBinderDied() u" + userId + "/" + packageName + " isPrimary: " + isPrimary); + + // First, disable hint mode for Auto profile and mark not BOUND for primary service ONLY. + if (isPrimary) { + final List<AssociationInfo> associations = + mAssociationStore.getActiveAssociationsByPackage(userId, packageName); + + for (AssociationInfo association : associations) { + final String deviceProfile = association.getDeviceProfile(); + if (DEVICE_PROFILE_AUTOMOTIVE_PROJECTION.equals(deviceProfile)) { + Slog.i(TAG, "Disable hint mode for device profile: " + deviceProfile); + mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false); + break; + } + } + + mCompanionAppBinder.removePackage(userId, packageName); + } + + // Second: schedule rebinding if needed. + final boolean shouldScheduleRebind = shouldScheduleRebind(userId, packageName, isPrimary); + + if (shouldScheduleRebind) { + mCompanionAppBinder.scheduleRebinding(userId, packageName, serviceConnector); + } + } + + /** + * Check if the system should rebind the self-managed secondary services + * OR non-self-managed services. + */ + private boolean shouldScheduleRebind(int userId, String packageName, boolean isPrimary) { + // Make sure do not schedule rebind for the case ServiceConnector still gets callback after + // app is uninstalled. + boolean stillAssociated = false; + // Make sure to clean up the state for all the associations + // that associate with this package. + boolean shouldScheduleRebind = false; + boolean shouldScheduleRebindForUuid = false; + final List<ObservableUuid> uuids = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + + for (AssociationInfo ai : + mAssociationStore.getActiveAssociationsByPackage(userId, packageName)) { + final int associationId = ai.getId(); + stillAssociated = true; + if (ai.isSelfManaged()) { + // Do not rebind if primary one is died for selfManaged application. + if (isPrimary && isDevicePresent(associationId)) { + onSelfManagedDeviceReporterBinderDied(associationId); + shouldScheduleRebind = false; + } + // Do not rebind if both primary and secondary services are died for + // selfManaged application. + shouldScheduleRebind = mCompanionAppBinder.isCompanionApplicationBound(userId, + packageName); + } else if (ai.isNotifyOnDeviceNearby()) { + // Always rebind for non-selfManaged devices. + shouldScheduleRebind = true; + } + } + + for (ObservableUuid uuid : uuids) { + if (isDeviceUuidPresent(uuid.getUuid())) { + shouldScheduleRebindForUuid = true; + break; + } + } + + return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid; + } + + /** + * Implements + * {@link AssociationStore.OnChangeListener#onAssociationRemoved(AssociationInfo)} + */ + @Override + public void onAssociationRemoved(@NonNull AssociationInfo association) { + final int id = association.getId(); + if (DEBUG) { + Log.i(TAG, "onAssociationRemoved() id=" + id); + Log.d(TAG, " > association=" + association); + } + + mConnectedBtDevices.remove(id); + mNearbyBleDevices.remove(id); + mReportedSelfManagedDevices.remove(id); + mSimulated.remove(id); + synchronized (mBtDisconnectedDevices) { + mBtDisconnectedDevices.remove(id); + mBtDisconnectedDevicesBlePresence.delete(id); + } + + // Do NOT call mCallback.onDeviceDisappeared()! + // CompanionDeviceManagerService will know that the association is removed, and will do + // what's needed. + } + + /** + * Return a set of devices that pending to report connectivity + */ + public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() { + synchronized (mBtConnectionListener.mPendingConnectedDevices) { + return mBtConnectionListener.mPendingConnectedDevices; + } + } + + private static void enforceCallerShellOrRoot() { + final int callingUid = Binder.getCallingUid(); + if (callingUid == SHELL_UID || callingUid == ROOT_UID) return; + + throw new SecurityException("Caller is neither Shell nor Root"); + } + + /** + * The BLE scan can be only stopped if all the devices have been reported + * BT connected and BLE presence and are not pending to report BLE lost. + */ + private boolean canStopBleScan() { + for (AssociationInfo ai : mAssociationStore.getActiveAssociations()) { + int id = ai.getId(); + synchronized (mBtDisconnectedDevices) { + if (ai.isNotifyOnDeviceNearby() && !(isBtConnected(id) + && isBlePresent(id) && mBtDisconnectedDevices.isEmpty())) { + Slog.i(TAG, "The BLE scan cannot be stopped, " + + "device( " + id + " ) is not yet connected " + + "OR the BLE is not current present Or is pending to report BLE lost"); + return false; + } + } + } + return true; + } + + /** + * Dumps system information about devices that are marked as "present". + */ + public void dump(@NonNull PrintWriter out) { + out.append("Companion Device Present: "); + if (mConnectedBtDevices.isEmpty() + && mNearbyBleDevices.isEmpty() + && mReportedSelfManagedDevices.isEmpty()) { + out.append("<empty>\n"); + return; + } else { + out.append("\n"); + } + + out.append(" Connected Bluetooth Devices: "); + if (mConnectedBtDevices.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int associationId : mConnectedBtDevices) { + AssociationInfo a = mAssociationStore.getAssociationById(associationId); + out.append(" ").append(a.toShortString()).append('\n'); + } + } + + out.append(" Nearby BLE Devices: "); + if (mNearbyBleDevices.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int associationId : mNearbyBleDevices) { + AssociationInfo a = mAssociationStore.getAssociationById(associationId); + out.append(" ").append(a.toShortString()).append('\n'); + } + } + + out.append(" Self-Reported Devices: "); + if (mReportedSelfManagedDevices.isEmpty()) { + out.append("<empty>\n"); + } else { + out.append("\n"); + for (int associationId : mReportedSelfManagedDevices) { + AssociationInfo a = mAssociationStore.getAssociationById(associationId); + out.append(" ").append(a.toShortString()).append('\n'); + } + } + } + + private class SimulatedDevicePresenceSchedulerHelper extends Handler { + SimulatedDevicePresenceSchedulerHelper() { + super(Looper.getMainLooper()); + } + + void scheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { + // First, unschedule if it was scheduled previously. + if (hasMessages(/* what */ associationId)) { + removeMessages(/* what */ associationId); + } + + sendEmptyMessageDelayed(/* what */ associationId, 60 * 1000 /* 60 seconds */); + } + + void unscheduleOnDeviceGoneCallForSimulatedDevicePresence(int associationId) { + removeMessages(/* what */ associationId); + } + + @Override + public void handleMessage(@NonNull Message msg) { + final int associationId = msg.what; + if (mSimulated.contains(associationId)) { + onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED); + } + } + } + + private class BleDeviceDisappearedScheduler extends Handler { + BleDeviceDisappearedScheduler() { + super(Looper.getMainLooper()); + } + + void scheduleBleDeviceDisappeared(int associationId) { + if (hasMessages(associationId)) { + removeMessages(associationId); + } + Slog.i(TAG, "scheduleBleDeviceDisappeared for Device: ( " + associationId + " )."); + sendEmptyMessageDelayed(associationId, 10 * 1000 /* 10 seconds */); + } + + void unScheduleDeviceDisappeared(int associationId) { + if (hasMessages(associationId)) { + Slog.i(TAG, "unScheduleDeviceDisappeared for Device( " + associationId + " )"); + synchronized (mBtDisconnectedDevices) { + mBtDisconnectedDevices.remove(associationId); + mBtDisconnectedDevicesBlePresence.delete(associationId); + } + + removeMessages(associationId); + } + } + + @Override + public void handleMessage(@NonNull Message msg) { + final int associationId = msg.what; + synchronized (mBtDisconnectedDevices) { + final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get( + associationId); + // If a device hasn't reported after 10 seconds and is not currently present, + // assume BLE is lost and trigger the onDeviceEvent callback with the + // EVENT_BLE_DISAPPEARED event. + if (mBtDisconnectedDevices.contains(associationId) + && !isCurrentPresent) { + Slog.i(TAG, "Device ( " + associationId + " ) is likely BLE out of range, " + + "sending callback with event ( " + EVENT_BLE_DISAPPEARED + " )"); + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); + } + + mBtDisconnectedDevices.remove(associationId); + mBtDisconnectedDevicesBlePresence.delete(associationId); + } + } + } +} diff --git a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java index db15da2922cf..fa0f6bd92acb 100644 --- a/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java +++ b/services/companion/java/com/android/server/companion/presence/ObservableUuidStore.java @@ -300,4 +300,18 @@ public class ObservableUuidStore { return readObservableUuidsFromCache(userId); } } + + /** + * Check if a UUID is being observed by the package. + */ + public boolean isUuidBeingObserved(ParcelUuid uuid, int userId, String packageName) { + final List<ObservableUuid> uuidsBeingObserved = getObservableUuidsForPackage(userId, + packageName); + for (ObservableUuid observableUuid : uuidsBeingObserved) { + if (observableUuid.getUuid().equals(uuid)) { + return true; + } + } + return false; + } } diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java index 793fb7ff74b1..697ef87b5a12 100644 --- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java +++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java @@ -46,7 +46,6 @@ import java.util.concurrent.Future; @SuppressLint("LongLogTag") public class CompanionTransportManager { private static final String TAG = "CDM_CompanionTransportManager"; - private static final boolean DEBUG = false; private boolean mSecureTransportEnabled = true; @@ -137,11 +136,17 @@ public class CompanionTransportManager { } } - public void attachSystemDataTransport(String packageName, int userId, int associationId, - ParcelFileDescriptor fd) { + /** + * Attach transport. + */ + public void attachSystemDataTransport(int associationId, ParcelFileDescriptor fd) { + Slog.i(TAG, "Attaching transport for association id=[" + associationId + "]..."); + + mAssociationStore.getAssociationWithCallerChecks(associationId); + synchronized (mTransports) { if (mTransports.contains(associationId)) { - detachSystemDataTransport(packageName, userId, associationId); + detachSystemDataTransport(associationId); } // TODO: Implement new API to pass a PSK @@ -149,9 +154,18 @@ public class CompanionTransportManager { notifyOnTransportsChanged(); } + + Slog.i(TAG, "Transport attached."); } - public void detachSystemDataTransport(String packageName, int userId, int associationId) { + /** + * Detach transport. + */ + public void detachSystemDataTransport(int associationId) { + Slog.i(TAG, "Detaching transport for association id=[" + associationId + "]..."); + + mAssociationStore.getAssociationWithCallerChecks(associationId); + synchronized (mTransports) { final Transport transport = mTransports.removeReturnOld(associationId); if (transport == null) { @@ -161,6 +175,8 @@ public class CompanionTransportManager { transport.stop(); notifyOnTransportsChanged(); } + + Slog.i(TAG, "Transport detached."); } private void notifyOnTransportsChanged() { @@ -307,8 +323,7 @@ public class CompanionTransportManager { int associationId = transport.mAssociationId; AssociationInfo association = mAssociationStore.getAssociationById(associationId); if (association != null) { - detachSystemDataTransport(association.getPackageName(), - association.getUserId(), + detachSystemDataTransport( association.getId()); } } diff --git a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java index 2cf1f462a7d1..d7e766eed209 100644 --- a/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java +++ b/services/companion/java/com/android/server/companion/utils/PermissionsUtils.java @@ -39,7 +39,6 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.companion.AssociationInfo; import android.companion.AssociationRequest; import android.companion.CompanionDeviceManager; import android.content.Context; @@ -208,7 +207,7 @@ public final class PermissionsUtils { /** * Require the caller to hold necessary permission to observe device presence by UUID. */ - public static void enforceCallerCanObservingDevicePresenceByUuid(@NonNull Context context) { + public static void enforceCallerCanObserveDevicePresenceByUuid(@NonNull Context context) { if (context.checkCallingPermission(REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) != PERMISSION_GRANTED) { throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have " @@ -235,23 +234,6 @@ public final class PermissionsUtils { return checkCallerCanManageCompanionDevice(context); } - /** - * Check if CDM can trust the context to process the association. - */ - @Nullable - public static AssociationInfo sanitizeWithCallerChecks(@NonNull Context context, - @Nullable AssociationInfo association) { - if (association == null) return null; - - final int userId = association.getUserId(); - final String packageName = association.getPackageName(); - if (!checkCallerCanManageAssociationsForPackage(context, userId, packageName)) { - return null; - } - - return association; - } - private static boolean checkPackage(@UserIdInt int uid, @NonNull String packageName) { try { return getAppOpsService().checkPackage(uid, packageName) == MODE_ALLOWED; diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 8244d20e8e6a..3ec6e475179a 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -23,6 +23,7 @@ import static android.companion.virtual.VirtualDeviceParams.ACTIVITY_POLICY_DEFA import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; @@ -82,6 +83,8 @@ import android.hardware.input.VirtualStylusConfig; import android.hardware.input.VirtualStylusMotionEvent; import android.hardware.input.VirtualTouchEvent; import android.hardware.input.VirtualTouchscreenConfig; +import android.media.AudioManager; +import android.media.audiopolicy.AudioMix; import android.os.Binder; import android.os.IBinder; import android.os.LocaleList; @@ -1063,6 +1066,37 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override + public boolean hasCustomAudioInputSupport() throws RemoteException { + if (!Flags.vdmPublicApis()) { + return false; + } + + if (!android.media.audiopolicy.Flags.audioMixTestApi()) { + return false; + } + if (!android.media.audiopolicy.Flags.recordAudioDeviceAwarePermission()) { + return false; + } + + if (getDevicePolicy(POLICY_TYPE_AUDIO) == VirtualDeviceParams.DEVICE_POLICY_DEFAULT) { + return false; + } + final long token = Binder.clearCallingIdentity(); + try { + AudioManager audioManager = mContext.getSystemService(AudioManager.class); + for (AudioMix mix : audioManager.getRegisteredPolicyMixes()) { + if (mix.matchesVirtualDeviceId(getDeviceId()) + && mix.getMixType() == AudioMix.MIX_TYPE_RECORDERS) { + return true; + } + } + } finally { + Binder.restoreCallingIdentity(token); + } + return false; + } + + @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { String indent = " "; fout.println(" VirtualDevice: "); diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java index 6b5ba96f6b1b..2607ed3193eb 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java @@ -1297,15 +1297,19 @@ public class ContentCaptureManagerService extends @Override public void onLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) { - RemoteContentProtectionService service = createRemoteContentProtectionService(); - if (service == null) { - return; - } - try { - service.onLoginDetected(events); - } catch (Exception ex) { - Slog.e(TAG, "Failed to call remote service", ex); - } + Binder.withCleanCallingIdentity( + () -> { + RemoteContentProtectionService service = + createRemoteContentProtectionService(); + if (service == null) { + return; + } + try { + service.onLoginDetected(events); + } catch (Exception ex) { + Slog.e(TAG, "Failed to call remote service", ex); + } + }); } } diff --git a/services/core/Android.bp b/services/core/Android.bp index d1d7ee7ba0e4..7f5867fb1a74 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -242,6 +242,7 @@ java_library_static { "apache-commons-math", "backstage_power_flags_lib", "notification_flags_lib", + "power_hint_flags_lib", "biometrics_flags_lib", "am_flags_lib", "com_android_server_accessibility_flags_lib", diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java index 08093c0c037f..e64a87f3966b 100644 --- a/services/core/java/android/content/pm/PackageManagerInternal.java +++ b/services/core/java/android/content/pm/PackageManagerInternal.java @@ -45,7 +45,6 @@ import android.util.SparseArray; import com.android.internal.pm.pkg.component.ParsedMainComponent; import com.android.internal.util.function.pooled.PooledLambda; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.KnownPackages; import com.android.server.pm.PackageArchiver; import com.android.server.pm.PackageList; @@ -1396,21 +1395,6 @@ public abstract class PackageManagerInternal { @UserIdInt int userId, @Nullable String recentCallingPackage, @NonNull String debugInfo); - /** @deprecated For legacy shell command only. */ - @Deprecated - public abstract void legacyDumpProfiles(@NonNull String packageName, - boolean dumpClassesAndMethods) throws LegacyDexoptDisabledException; - - /** @deprecated For legacy shell command only. */ - @Deprecated - public abstract void legacyForceDexOpt(@NonNull String packageName) - throws LegacyDexoptDisabledException; - - /** @deprecated For legacy shell command only. */ - @Deprecated - public abstract void legacyReconcileSecondaryDexFiles(String packageName) - throws LegacyDexoptDisabledException; - /** * Gets {@link PackageManager.DistractionRestriction restrictions} of the given * packages of the given user. diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java index ecd14ce67d7e..589d8b373802 100644 --- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java +++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java @@ -69,6 +69,8 @@ public final class SensitiveContentProtectionManagerService extends SystemServic final Object mSensitiveContentProtectionLock = new Object(); + private final ArraySet<PackageInfo> mPackagesShowingSensitiveContent = new ArraySet<>(); + @GuardedBy("mSensitiveContentProtectionLock") private boolean mProjectionActive = false; @@ -77,23 +79,24 @@ public final class SensitiveContentProtectionManagerService extends SystemServic @Override public void onStart(MediaProjectionInfo info) { if (DEBUG) Log.d(TAG, "onStart projection: " + info); - Trace.beginSection( + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SensitiveContentProtectionManagerService.onProjectionStart"); try { onProjectionStart(info.getPackageName()); } finally { - Trace.endSection(); + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } @Override public void onStop(MediaProjectionInfo info) { if (DEBUG) Log.d(TAG, "onStop projection: " + info); - Trace.beginSection("SensitiveContentProtectionManagerService.onProjectionStop"); + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, + "SensitiveContentProtectionManagerService.onProjectionStop"); try { onProjectionEnd(); } finally { - Trace.endSection(); + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } }; @@ -204,6 +207,10 @@ public final class SensitiveContentProtectionManagerService extends SystemServic if (sensitiveNotificationAppProtection()) { updateAppsThatShouldBlockScreenCapture(); } + + if (sensitiveContentAppProtection() && mPackagesShowingSensitiveContent.size() > 0) { + mWindowManager.addBlockScreenCaptureForApps(mPackagesShowingSensitiveContent); + } } } @@ -285,7 +292,8 @@ public final class SensitiveContentProtectionManagerService extends SystemServic @Override public void onListenerConnected() { super.onListenerConnected(); - Trace.beginSection("SensitiveContentProtectionManagerService.onListenerConnected"); + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, + "SensitiveContentProtectionManagerService.onListenerConnected"); try { // Projection started before notification listener was connected synchronized (mSensitiveContentProtectionLock) { @@ -294,14 +302,15 @@ public final class SensitiveContentProtectionManagerService extends SystemServic } } } finally { - Trace.endSection(); + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } @Override public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { super.onNotificationPosted(sbn, rankingMap); - Trace.beginSection("SensitiveContentProtectionManagerService.onNotificationPosted"); + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, + "SensitiveContentProtectionManagerService.onNotificationPosted"); try { synchronized (mSensitiveContentProtectionLock) { if (!mProjectionActive) { @@ -317,14 +326,14 @@ public final class SensitiveContentProtectionManagerService extends SystemServic } } } finally { - Trace.endSection(); + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } @Override public void onNotificationRankingUpdate(RankingMap rankingMap) { super.onNotificationRankingUpdate(rankingMap); - Trace.beginSection( + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SensitiveContentProtectionManagerService.onNotificationRankingUpdate"); try { synchronized (mSensitiveContentProtectionLock) { @@ -333,7 +342,7 @@ public final class SensitiveContentProtectionManagerService extends SystemServic } } } finally { - Trace.endSection(); + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } } @@ -351,17 +360,27 @@ public final class SensitiveContentProtectionManagerService extends SystemServic void setSensitiveContentProtection(IBinder windowToken, String packageName, int uid, boolean isShowingSensitiveContent) { synchronized (mSensitiveContentProtectionLock) { + // The window token distinguish this package from packages added for notifications. + PackageInfo packageInfo = new PackageInfo(packageName, uid, windowToken); + // track these packages to protect when screen share starts. + if (isShowingSensitiveContent) { + mPackagesShowingSensitiveContent.add(packageInfo); + if (mPackagesShowingSensitiveContent.size() > 100) { + Log.w(TAG, "Unexpectedly large number of sensitive windows, count: " + + mPackagesShowingSensitiveContent.size()); + } + } else { + mPackagesShowingSensitiveContent.remove(packageInfo); + } if (!mProjectionActive) { return; } if (DEBUG) { - Log.d(TAG, "setSensitiveContentProtection - windowToken=" + windowToken - + ", package=" + packageName + ", uid=" + uid - + ", isShowingSensitiveContent=" + isShowingSensitiveContent); + Log.d(TAG, "setSensitiveContentProtection - current package=" + packageInfo + + ", isShowingSensitiveContent=" + isShowingSensitiveContent + + ", sensitive packages=" + mPackagesShowingSensitiveContent); } - // The window token distinguish this package from packages added for notifications. - PackageInfo packageInfo = new PackageInfo(packageName, uid, windowToken); ArraySet<PackageInfo> packageInfos = new ArraySet<>(); packageInfos.add(packageInfo); if (isShowingSensitiveContent) { @@ -382,20 +401,26 @@ public final class SensitiveContentProtectionManagerService extends SystemServic public void setSensitiveContentProtection(IBinder windowToken, String packageName, boolean isShowingSensitiveContent) { - Trace.beginSection( + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SensitiveContentProtectionManagerService.setSensitiveContentProtection"); try { int callingUid = Binder.getCallingUid(); verifyCallingPackage(callingUid, packageName); final long identity = Binder.clearCallingIdentity(); try { + if (isShowingSensitiveContent + && mWindowManager.getWindowName(windowToken) == null) { + Log.e(TAG, "window token is not know to WMS, can't apply protection," + + " token: " + windowToken + ", package: " + packageName); + return; + } SensitiveContentProtectionManagerService.this.setSensitiveContentProtection( windowToken, packageName, callingUid, isShowingSensitiveContent); } finally { Binder.restoreCallingIdentity(identity); } } finally { - Trace.endSection(); + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); } } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 133a77df3573..04dd2f3fa288 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -8154,7 +8154,7 @@ public final class ActiveServices { BackgroundStartPrivileges.NONE); @ReasonCode int allowStartFgs = shouldAllowFgsStartForegroundNoBindingCheckLocked( allowWhileInUse, callingPid, callingUid, callingPackage, null /* targetService */, - BackgroundStartPrivileges.NONE); + BackgroundStartPrivileges.NONE, null); if (allowStartFgs == REASON_DENIED) { if (canBindingClientStartFgsLocked(callingUid) != null) { @@ -8410,7 +8410,8 @@ public final class ActiveServices { allowWhileInUse2, clientPid, clientUid, clientPackageName, null /* targetService */, - BackgroundStartPrivileges.NONE); + BackgroundStartPrivileges.NONE, + pr); if (allowStartFgs != REASON_DENIED) { return new Pair<>(allowStartFgs, clientPackageName); } else { @@ -8447,7 +8448,7 @@ public final class ActiveServices { ActivityManagerService.FgsTempAllowListItem tempAllowListReason = r.mInfoTempFgsAllowListReason = mAm.isAllowlistedForFgsStartLOSP(callingUid); int ret = shouldAllowFgsStartForegroundNoBindingCheckLocked(allowWhileInUse, callingPid, - callingUid, callingPackage, r, backgroundStartPrivileges); + callingUid, callingPackage, r, backgroundStartPrivileges, null); // If an app (App 1) is bound by another app (App 2) that could start an FGS, then App 1 // is also allowed to start an FGS. We check all the binding @@ -8503,7 +8504,8 @@ public final class ActiveServices { private @ReasonCode int shouldAllowFgsStartForegroundNoBindingCheckLocked( @ReasonCode int allowWhileInUse, int callingPid, int callingUid, String callingPackage, @Nullable ServiceRecord targetService, - BackgroundStartPrivileges backgroundStartPrivileges) { + BackgroundStartPrivileges backgroundStartPrivileges, + @Nullable ProcessRecord targetRecord) { int ret = allowWhileInUse; if (ret == REASON_DENIED) { @@ -8565,13 +8567,15 @@ public final class ActiveServices { if (ret == REASON_DENIED) { // Flag check: are we disabling SAW FGS background starts? final boolean shouldDisableSaw = Flags.fgsDisableSaw() - && CompatChanges.isChangeEnabled(FGS_BOOT_COMPLETED_RESTRICTIONS, callingUid); + && CompatChanges.isChangeEnabled(FGS_SAW_RESTRICTIONS, callingUid); if (shouldDisableSaw) { - final ProcessRecord processRecord = mAm - .getProcessRecordLocked(targetService.processName, - targetService.appInfo.uid); - if (processRecord != null) { - if (processRecord.mState.hasOverlayUi()) { + if (targetRecord == null) { + synchronized (mAm.mPidsSelfLocked) { + targetRecord = mAm.mPidsSelfLocked.get(callingPid); + } + } + if (targetRecord != null) { + if (targetRecord.mState.hasOverlayUi()) { if (mAm.mAtmInternal.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) { ret = REASON_SYSTEM_ALERT_WINDOW_PERMISSION; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 03ab5b32586e..4f1a35c3fbd4 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -2656,6 +2656,11 @@ public class ActivityManagerService extends IActivityManager.Stub return mBackgroundLaunchBroadcasts; } + private String getWearRemoteIntentAction() { + return mContext.getResources().getString( + com.android.internal.R.string.config_wearRemoteIntentAction); + } + /** * Ensures that the given package name has an explicit set of allowed associations. * If it does not, give it an empty set. @@ -9973,7 +9978,7 @@ public class ActivityManagerService extends IActivityManager.Stub "getHistoricalProcessStartReasons"); if (uid != INVALID_UID) { mProcessList.getAppStartInfoTracker().getStartInfo( - packageName, userId, callingPid, maxNum, results); + packageName, uid, callingPid, maxNum, results); } } else { // If no package name is given, use the caller's uid as the filter uid. @@ -15213,6 +15218,18 @@ public class ActivityManagerService extends IActivityManager.Stub intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); } + // TODO: b/329211459 - Remove this after background remote intent is fixed. + if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH) + && getWearRemoteIntentAction().equals(action)) { + final int callerProcState = callerApp != null + ? callerApp.getCurProcState() + : ActivityManager.PROCESS_STATE_NONEXISTENT; + if (ActivityManager.RunningAppProcessInfo.procStateToImportance(callerProcState) + > ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + return ActivityManager.START_CANCELED; + } + } + switch (action) { case Intent.ACTION_MEDIA_SCANNER_SCAN_FILE: UserManagerInternal umInternal = LocalServices.getService( @@ -19630,7 +19647,7 @@ public class ActivityManagerService extends IActivityManager.Stub record.procStateSeqWaitingForNetwork = 0; final long totalTime = SystemClock.uptimeMillis() - startTime; if (totalTime >= mConstants.mNetworkAccessTimeoutMs || DEBUG_NETWORK) { - Slog.w(TAG_NETWORK, "Total time waited for network rules to get updated: " + Slog.wtf(TAG_NETWORK, "Total time waited for network rules to get updated: " + totalTime + ". Uid: " + callingUid + " procStateSeq: " + procStateSeq + " UidRec: " + record + " validateUidRec: " diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 4ebabdc4cc66..5a97e87f53f7 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -1164,8 +1164,7 @@ final class ActivityManagerShellCommand extends ShellCommand { synchronized (mInternal) { synchronized (mInternal.mProcLock) { app.mOptRecord.setFreezeSticky(isSticky); - mInternal.mOomAdjuster.mCachedAppOptimizer.freezeAppAsyncInternalLSP( - app, 0 /* delayMillis */, true /* force */, false /* immediate */); + mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(app); } } return 0; diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java index 91cfb8fe45eb..e676b1fca7fb 100644 --- a/services/core/java/com/android/server/am/BroadcastConstants.java +++ b/services/core/java/com/android/server/am/BroadcastConstants.java @@ -281,7 +281,7 @@ public class BroadcastConstants { * For {@link BroadcastQueueModernImpl}: Maximum number of outgoing broadcasts from a * freezable process that will be allowed before killing the process. */ - public long MAX_FROZEN_OUTGOING_BROADCASTS = DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS; + public int MAX_FROZEN_OUTGOING_BROADCASTS = DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS; private static final String KEY_MAX_FROZEN_OUTGOING_BROADCASTS = "max_frozen_outgoing_broadcasts"; private static final int DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS = 32; diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index e98e1ba6a44e..ed3cd1ea03c8 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -277,6 +277,10 @@ class BroadcastProcessQueue { mOutgoingBroadcasts.clear(); } + public void clearOutgoingBroadcasts() { + mOutgoingBroadcasts.clear(); + } + /** * Enqueue the given broadcast to be dispatched to this process at some * future point in time. The target receiver is indicated by the given index diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index a6f6b3422066..c08288901157 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -166,7 +166,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { /** * Map from UID to per-process broadcast queues. If a UID hosts more than * one process, each additional process is stored as a linked list using - * {@link BroadcastProcessQueue#next}. + * {@link BroadcastProcessQueue#processNameNext}. * * @see #getProcessQueue * @see #getOrCreateProcessQueue @@ -661,6 +661,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { final BroadcastProcessQueue queue = getProcessQueue(app); if (queue != null) { setQueueProcess(queue, app); + // Outgoing broadcasts should be cleared when the process dies but there have been + // issues due to AMS not always informing the BroadcastQueue of process deaths. + // So, clear them when a new process starts as well. + queue.clearOutgoingBroadcasts(); } boolean didSomething = false; @@ -730,6 +734,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { demoteFromRunningLocked(queue); } + queue.clearOutgoingBroadcasts(); + // Skip any pending registered receivers, since the old process // would never be around to receive them boolean didSomething = queue.forEachMatchingBroadcast((r, i) -> { @@ -781,8 +787,11 @@ class BroadcastQueueModernImpl extends BroadcastQueue { final BroadcastProcessQueue queue = getOrCreateProcessQueue( r.callerApp.processName, r.callerApp.uid); if (queue.getOutgoingBroadcastCount() >= mConstants.MAX_FROZEN_OUTGOING_BROADCASTS) { - // TODO: Kill the process if the outgoing broadcasts count is - // beyond a certain limit. + r.callerApp.killLocked("Too many outgoing broadcasts in cached state", + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED, + true /* noisy */); + return; } queue.enqueueOutgoingBroadcast(r); mHistory.onBroadcastFrozenLocked(r); diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index 66abb4238726..b8ef03f36c23 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -19,6 +19,7 @@ package com.android.server.am; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; import static com.android.server.am.ActivityManagerService.checkComponentPermission; import static com.android.server.am.BroadcastQueue.TAG; +import static com.android.server.am.Flags.usePermissionManagerForBroadcastDeliveryCheck; import android.annotation.NonNull; import android.annotation.Nullable; @@ -27,6 +28,7 @@ import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.PendingIntent; +import android.content.AttributionSource; import android.content.ComponentName; import android.content.IIntentSender; import android.content.Intent; @@ -39,6 +41,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.permission.IPermissionManager; +import android.permission.PermissionManager; import android.util.Slog; import com.android.internal.util.ArrayUtils; @@ -54,6 +57,9 @@ import java.util.Objects; public class BroadcastSkipPolicy { private final ActivityManagerService mService; + @Nullable + private PermissionManager mPermissionManager; + public BroadcastSkipPolicy(@NonNull ActivityManagerService service) { mService = Objects.requireNonNull(service); } @@ -283,14 +289,35 @@ public class BroadcastSkipPolicy { if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID && r.requiredPermissions != null && r.requiredPermissions.length > 0) { + final AttributionSource attributionSource; + if (usePermissionManagerForBroadcastDeliveryCheck()) { + attributionSource = + new AttributionSource.Builder(info.activityInfo.applicationInfo.uid) + .setPackageName(info.activityInfo.packageName) + .build(); + } else { + attributionSource = null; + } for (int i = 0; i < r.requiredPermissions.length; i++) { String requiredPermission = r.requiredPermissions[i]; try { - perm = AppGlobals.getPackageManager(). - checkPermission(requiredPermission, - info.activityInfo.applicationInfo.packageName, - UserHandle - .getUserId(info.activityInfo.applicationInfo.uid)); + if (usePermissionManagerForBroadcastDeliveryCheck()) { + final PermissionManager permissionManager = getPermissionManager(); + if (permissionManager != null) { + perm = permissionManager.checkPermissionForDataDelivery( + requiredPermission, attributionSource, null /* message */); + } else { + // Assume permission denial if PermissionManager is not yet available. + perm = PackageManager.PERMISSION_DENIED; + } + } else { + perm = AppGlobals.getPackageManager() + .checkPermission( + requiredPermission, + info.activityInfo.applicationInfo.packageName, + UserHandle + .getUserId(info.activityInfo.applicationInfo.uid)); + } } catch (RemoteException e) { perm = PackageManager.PERMISSION_DENIED; } @@ -302,11 +329,13 @@ public class BroadcastSkipPolicy { + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")"; } - int appOp = AppOpsManager.permissionToOpCode(requiredPermission); - if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) { - if (!noteOpForManifestReceiver(appOp, r, info, component)) { - return "Skipping delivery to " + info.activityInfo.packageName - + " due to required appop " + appOp; + if (!usePermissionManagerForBroadcastDeliveryCheck()) { + int appOp = AppOpsManager.permissionToOpCode(requiredPermission); + if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) { + if (!noteOpForManifestReceiver(appOp, r, info, component)) { + return "Skipping delivery to " + info.activityInfo.packageName + + " due to required appop " + appOp; + } } } } @@ -694,4 +723,11 @@ public class BroadcastSkipPolicy { return false; } + + private PermissionManager getPermissionManager() { + if (mPermissionManager == null) { + mPermissionManager = mService.mContext.getSystemService(PermissionManager.class); + } + return mPermissionManager; + } } diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 0cf557588958..6e20f6cc877d 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -1414,8 +1414,13 @@ public final class CachedAppOptimizer { } @GuardedBy({"mAm", "mProcLock"}) + void forceFreezeAppAsyncLSP(ProcessRecord app) { + freezeAppAsyncInternalLSP(app, 0 /* delayMillis */, true /* force */); + } + + @GuardedBy({"mAm", "mProcLock"}) private void freezeAppAsyncLSP(ProcessRecord app, @UptimeMillisLong long delayMillis) { - freezeAppAsyncInternalLSP(app, delayMillis, false, false); + freezeAppAsyncInternalLSP(app, delayMillis, false /* force */); } @GuardedBy({"mAm", "mProcLock"}) @@ -1427,17 +1432,18 @@ public final class CachedAppOptimizer { // and remove this method. @GuardedBy({"mAm", "mProcLock"}) void freezeAppAsyncImmediateLSP(ProcessRecord app) { - freezeAppAsyncInternalLSP(app, 0, false, true); + freezeAppAsyncInternalLSP(app, 0 /* delayMillis */, false /* force */); } - // TODO: Update this method to be private and have the existing clients call different methods. - // This "internal" method should not be directly triggered by clients outside this class. @GuardedBy({"mAm", "mProcLock"}) - void freezeAppAsyncInternalLSP(ProcessRecord app, @UptimeMillisLong long delayMillis, - boolean force, boolean immediate) { + private void freezeAppAsyncInternalLSP(ProcessRecord app, @UptimeMillisLong long delayMillis, + boolean force) { final ProcessCachedOptimizerRecord opt = app.mOptRecord; if (opt.isPendingFreeze()) { - if (immediate) { + if (delayMillis == 0) { + // Caller is requesting to freeze the process without delay, so remove + // any already posted messages which would have been handled with a delay and + // post a new message without a delay. mFreezeHandler.removeMessages(SET_FROZEN_PROCESS_MSG, app); mFreezeHandler.sendMessage(mFreezeHandler.obtainMessage( SET_FROZEN_PROCESS_MSG, DO_FREEZE, 0, app)); diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 7df5fdd282c3..48d3c09290ce 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -127,6 +127,7 @@ public class SettingsToPropertiesMapper { "avic", "bluetooth", "brownout_mitigation_audio", + "brownout_mitigation_modem", "build", "biometrics", "biometrics_framework", @@ -168,6 +169,7 @@ public class SettingsToPropertiesMapper { "pixel_biometrics_face", "pixel_bluetooth", "pixel_connectivity_gps", + "pixel_sensors", "pixel_system_sw_video", "pixel_watch", "platform_compat", diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 0209944f9fd0..fd847f11157f 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -86,3 +86,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "backstage_power" + name: "use_permission_manager_for_broadcast_delivery_check" + description: "Use PermissionManager API for broadcast delivery permission checks." + bug: "315468967" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index de000bf64c38..0f9517460ee9 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -6854,15 +6854,6 @@ public class AudioService extends IAudioService.Stub ringerMode = RINGER_MODE_SILENT; } } - } else if (mIsSingleVolume && (direction == AudioManager.ADJUST_TOGGLE_MUTE - || direction == AudioManager.ADJUST_MUTE)) { - if (mHasVibrator) { - ringerMode = RINGER_MODE_VIBRATE; - } else { - ringerMode = RINGER_MODE_SILENT; - } - // Setting the ringer mode will toggle mute - result &= ~FLAG_ADJUST_VOLUME; } break; case RINGER_MODE_VIBRATE: @@ -6871,11 +6862,8 @@ public class AudioService extends IAudioService.Stub "but no vibrator is present"); break; } - if ((direction == AudioManager.ADJUST_LOWER)) { - // This is the case we were muted with the volume turned up - if (mIsSingleVolume && oldIndex >= 2 * step && isMuted) { - ringerMode = RINGER_MODE_NORMAL; - } else if (mPrevVolDirection != AudioManager.ADJUST_LOWER) { + if (direction == AudioManager.ADJUST_LOWER) { + if (mPrevVolDirection != AudioManager.ADJUST_LOWER) { if (mVolumePolicy.volumeDownToEnterSilent) { final long diff = SystemClock.uptimeMillis() - mLoweredFromNormalToVibrateTime; @@ -6895,10 +6883,7 @@ public class AudioService extends IAudioService.Stub result &= ~FLAG_ADJUST_VOLUME; break; case RINGER_MODE_SILENT: - if (mIsSingleVolume && direction == AudioManager.ADJUST_LOWER && oldIndex >= 2 * step && isMuted) { - // This is the case we were muted with the volume turned up - ringerMode = RINGER_MODE_NORMAL; - } else if (direction == AudioManager.ADJUST_RAISE + if (direction == AudioManager.ADJUST_RAISE || direction == AudioManager.ADJUST_TOGGLE_MUTE || direction == AudioManager.ADJUST_UNMUTE) { if (!mVolumePolicy.volumeUpToExitSilent) { @@ -12375,7 +12360,8 @@ public class AudioService extends IAudioService.Stub } private boolean callerHasPermission(String permission) { - return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED; + return mContext.checkCallingOrSelfPermission(permission) + == PackageManager.PERMISSION_GRANTED; } /** @return true if projection is a valid MediaProjection that can project audio. */ diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index c5073001a672..3d4801b3e9aa 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -831,6 +831,7 @@ public final class AuthSession implements IBinder.DeathRecipient { break; case BiometricPrompt.DISMISSED_REASON_NEGATIVE: + case BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS: mClientReceiver.onDialogDismissed(reason); break; diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index 506b4562b43d..62c21cf36f69 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -256,10 +256,10 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions> // For BP, BiometricService will add the authToken to Keystore. if (!isBiometricPrompt() && mIsStrongBiometric) { final int result = KeyStore.getInstance().addAuthToken(byteToken); - if (result != KeyStore.NO_ERROR) { + if (result != 0) { Slog.d(TAG, "Error adding auth token : " + result); } else { - Slog.d(TAG, "addAuthToken: " + result); + Slog.d(TAG, "addAuthToken succeeded"); } } else { Slog.d(TAG, "Skipping addAuthToken"); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index fb826c824354..11db18359f23 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -895,7 +895,13 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { for (int i = 0; i < mFaceSensors.size(); i++) { final Sensor sensor = mFaceSensors.valueAt(i); final int sensorId = mFaceSensors.keyAt(i); - PerformanceTracker.getInstanceForSensorId(sensorId).incrementHALDeathCount(); + final PerformanceTracker performanceTracker = PerformanceTracker.getInstanceForSensorId( + sensorId); + if (performanceTracker != null) { + performanceTracker.incrementHALDeathCount(); + } else { + Slog.w(getTag(), "Performance tracker is null. Not counting HAL death."); + } sensor.onBinderDied(); } }); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index c04c47e2d95a..9290f8a48b79 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -954,7 +954,13 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi for (int i = 0; i < mFingerprintSensors.size(); i++) { final Sensor sensor = mFingerprintSensors.valueAt(i); final int sensorId = mFingerprintSensors.keyAt(i); - PerformanceTracker.getInstanceForSensorId(sensorId).incrementHALDeathCount(); + final PerformanceTracker performanceTracker = PerformanceTracker.getInstanceForSensorId( + sensorId); + if (performanceTracker != null) { + performanceTracker.incrementHALDeathCount(); + } else { + Slog.w(getTag(), "Performance tracker is null. Not counting HAL death."); + } sensor.onBinderDied(); } }); diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java index d93ff9dac91f..086f3aa8ad65 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java @@ -138,7 +138,9 @@ public final class BroadcastRadioServiceImpl { /** * Constructs BroadcastRadioServiceImpl using AIDL HAL using the list of names of AIDL - * BroadcastRadio HAL services {@code serviceNameList} + * BroadcastRadio HAL services + * + * @param serviceNameList list of names of AIDL BroadcastRadio HAL services */ public BroadcastRadioServiceImpl(ArrayList<String> serviceNameList) { mNextModuleId = 0; @@ -169,7 +171,11 @@ public final class BroadcastRadioServiceImpl { } /** - * Gets the AIDL RadioModule for the given {@code moduleId}. Null will be returned if not found. + * Gets the AIDL RadioModule for the given module Id. + * + * @param id Id of {@link RadioModule} of AIDL BroadcastRadio HAL service + * @return {@code true} if {@link RadioModule} of AIDL BroadcastRadio HAL service is found, + * {@code false} otherwise */ public boolean hasModule(int id) { synchronized (mLock) { diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index b7ece2ea65b1..5905b7de5b6e 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -366,7 +366,6 @@ public class Vpn { private PendingIntent mStatusIntent; private volatile boolean mEnableTeardown = true; - private final INetworkManagementService mNms; private final INetd mNetd; @VisibleForTesting @GuardedBy("this") @@ -626,7 +625,6 @@ public class Vpn { mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); mDeps = deps; - mNms = netService; mNetd = netd; mUserId = userId; mLooper = looper; diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 40325146ca25..4aab9d26dbcb 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -54,6 +54,7 @@ import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.os.BackgroundThread; import com.android.server.EventLogTags; import com.android.server.display.brightness.BrightnessEvent; +import com.android.server.display.brightness.clamper.BrightnessClamperController; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -252,6 +253,7 @@ public class AutomaticBrightnessController { // Controls Brightness range (including High Brightness Mode). private final BrightnessRangeController mBrightnessRangeController; + private final BrightnessClamperController mBrightnessClamperController; // Throttles (caps) maximum allowed brightness private final BrightnessThrottler mBrightnessThrottler; @@ -287,7 +289,8 @@ public class AutomaticBrightnessController { HysteresisLevels screenBrightnessThresholdsIdle, Context context, BrightnessRangeController brightnessModeController, BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort, - int ambientLightHorizonLong, float userLux, float userNits) { + int ambientLightHorizonLong, float userLux, float userNits, + BrightnessClamperController brightnessClamperController) { this(new Injector(), callbacks, looper, sensorManager, lightSensor, brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate, @@ -297,7 +300,7 @@ public class AutomaticBrightnessController { screenBrightnessThresholds, ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, context, brightnessModeController, brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux, - userNits + userNits, brightnessClamperController ); } @@ -313,9 +316,10 @@ public class AutomaticBrightnessController { HysteresisLevels screenBrightnessThresholds, HysteresisLevels ambientBrightnessThresholdsIdle, HysteresisLevels screenBrightnessThresholdsIdle, Context context, - BrightnessRangeController brightnessModeController, + BrightnessRangeController brightnessRangeController, BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort, - int ambientLightHorizonLong, float userLux, float userNits) { + int ambientLightHorizonLong, float userLux, float userNits, + BrightnessClamperController brightnessClamperController) { mInjector = injector; mClock = injector.createClock(); mContext = context; @@ -358,7 +362,8 @@ public class AutomaticBrightnessController { mPendingForegroundAppPackageName = null; mForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED; mPendingForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED; - mBrightnessRangeController = brightnessModeController; + mBrightnessRangeController = brightnessRangeController; + mBrightnessClamperController = brightnessClamperController; mBrightnessThrottler = brightnessThrottler; mBrightnessMappingStrategyMap = brightnessMappingStrategyMap; @@ -791,7 +796,7 @@ public class AutomaticBrightnessController { mAmbientBrightnessThresholds.getDarkeningThreshold(lux); } mBrightnessRangeController.onAmbientLuxChange(mAmbientLux); - + mBrightnessClamperController.onAmbientLuxChange(mAmbientLux); // If the short term model was invalidated and the change is drastic enough, reset it. mShortTermModel.maybeReset(mAmbientLux); diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 411666942b6d..9950d8ff42ed 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -61,6 +61,7 @@ import com.android.server.display.config.IdleScreenRefreshRateTimeout; import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint; import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholds; import com.android.server.display.config.IntegerArray; +import com.android.server.display.config.LowBrightnessData; import com.android.server.display.config.LuxThrottling; import com.android.server.display.config.NitsMap; import com.android.server.display.config.NonNegativeFloatToFloatPoint; @@ -555,6 +556,24 @@ import javax.xml.datatype.DatatypeConfigurationException; * <majorVersion>2</majorVersion> * <minorVersion>0</minorVersion> * </usiVersion> + * <lowBrightness enabled="true"> + * <transitionPoint>0.1</transitionPoint> + * + * <nits>0.2</nits> + * <nits>2.0</nits> + * <nits>500.0</nits> + * <nits>1000.0</nits> + * + * <backlight>0</backlight> + * <backlight>0.0001</backlight> + * <backlight>0.5</backlight> + * <backlight>1.0</backlight> + * + * <brightness>0</brightness> + * <brightness>0.1</brightness> + * <brightness>0.5</brightness> + * <brightness>1.0</brightness> + * </lowBrightness> * <screenBrightnessCapForWearBedtimeMode>0.1</screenBrightnessCapForWearBedtimeMode> * <idleScreenRefreshRateTimeout> * <luxThresholds> @@ -568,6 +587,8 @@ import javax.xml.datatype.DatatypeConfigurationException; * </point> * </luxThresholds> * </idleScreenRefreshRateTimeout> + * + * * </displayConfiguration> * } * </pre> @@ -732,6 +753,7 @@ public class DisplayDeviceConfig { private Spline mBacklightToBrightnessSpline; private Spline mBacklightToNitsSpline; private Spline mNitsToBacklightSpline; + private List<String> mQuirks; private boolean mIsHighBrightnessModeEnabled = false; private HighBrightnessModeData mHbmData; @@ -872,6 +894,10 @@ public class DisplayDeviceConfig { @Nullable private HdrBrightnessData mHdrBrightnessData; + // Null if low brightness mode is disabled - in config or by flag. + @Nullable + public LowBrightnessData mLowBrightnessData; + /** * Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode. */ @@ -1038,6 +1064,9 @@ public class DisplayDeviceConfig { * @return The brightness mapping nits array. */ public float[] getNits() { + if (mLowBrightnessData != null) { + return mLowBrightnessData.mNits; + } return mNits; } @@ -1046,7 +1075,11 @@ public class DisplayDeviceConfig { * * @return The backlight mapping value array. */ + @VisibleForTesting public float[] getBacklight() { + if (mLowBrightnessData != null) { + return mLowBrightnessData.mBacklight; + } return mBacklight; } @@ -1058,9 +1091,26 @@ public class DisplayDeviceConfig { * @return backlight value on the HAL scale of 0-1 */ public float getBacklightFromBrightness(float brightness) { + if (mLowBrightnessData != null) { + return mLowBrightnessData.mBrightnessToBacklight.interpolate(brightness); + } return mBrightnessToBacklightSpline.interpolate(brightness); } + private float getBrightnessFromBacklight(float brightness) { + if (mLowBrightnessData != null) { + return mLowBrightnessData.mBacklightToBrightness.interpolate(brightness); + } + return mBacklightToBrightnessSpline.interpolate(brightness); + } + + private Spline getBacklightToBrightnessSpline() { + if (mLowBrightnessData != null) { + return mLowBrightnessData.mBacklightToBrightness; + } + return mBacklightToBrightnessSpline; + } + /** * Calculates the nits value for the specified backlight value if a mapping exists. * @@ -1068,6 +1118,14 @@ public class DisplayDeviceConfig { * exits. */ public float getNitsFromBacklight(float backlight) { + if (mLowBrightnessData != null) { + if (mLowBrightnessData.mBacklightToNits == null) { + return INVALID_NITS; + } + backlight = Math.max(backlight, mBacklightMinimum); + return mLowBrightnessData.mBacklightToNits.interpolate(backlight); + } + if (mBacklightToNitsSpline == null) { return INVALID_NITS; } @@ -1075,6 +1133,20 @@ public class DisplayDeviceConfig { return mBacklightToNitsSpline.interpolate(backlight); } + private float getBacklightFromNits(float nits) { + if (mLowBrightnessData != null) { + return mLowBrightnessData.mNitsToBacklight.interpolate(nits); + } + return mNitsToBacklightSpline.interpolate(nits); + } + + private Spline getNitsToBacklightSpline() { + if (mLowBrightnessData != null) { + return mLowBrightnessData.mNitsToBacklight; + } + return mNitsToBacklightSpline; + } + /** * @return true if there is sdrHdrRatioMap, false otherwise. */ @@ -1101,13 +1173,13 @@ public class DisplayDeviceConfig { float ratio = Math.min(mSdrToHdrRatioSpline.interpolate(nits), maxDesiredHdrSdrRatio); float hdrNits = nits * ratio; - if (mNitsToBacklightSpline == null) { + if (getNitsToBacklightSpline() == null) { return PowerManager.BRIGHTNESS_INVALID; } - float hdrBacklight = mNitsToBacklightSpline.interpolate(hdrNits); + float hdrBacklight = getBacklightFromNits(hdrNits); hdrBacklight = Math.max(mBacklightMinimum, Math.min(mBacklightMaximum, hdrBacklight)); - float hdrBrightness = mBacklightToBrightnessSpline.interpolate(hdrBacklight); + float hdrBrightness = getBrightnessFromBacklight(hdrBacklight); if (DEBUG) { Slog.d(TAG, "getHdrBrightnessFromSdr: sdr brightness " + brightness @@ -1129,6 +1201,9 @@ public class DisplayDeviceConfig { * @return brightness array */ public float[] getBrightness() { + if (mLowBrightnessData != null) { + return mLowBrightnessData.mBrightness; + } return mBrightness; } @@ -1814,6 +1889,15 @@ public class DisplayDeviceConfig { } /** + * + * @return true if low brightness mode is enabled + */ + @VisibleForTesting + public boolean getLbmEnabled() { + return mLowBrightnessData != null; + } + + /** * @return Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode. */ public float getBrightnessCapForWearBedtimeMode() { @@ -1952,6 +2036,9 @@ public class DisplayDeviceConfig { + "mUsiVersion= " + mHostUsiVersion + "\n" + "mHdrBrightnessData= " + mHdrBrightnessData + "\n" + "mBrightnessCapForWearBedtimeMode= " + mBrightnessCapForWearBedtimeMode + + "\n" + + "mLowBrightnessData:" + (mLowBrightnessData != null + ? mLowBrightnessData.toString() : "null") + "}"; } @@ -2002,6 +2089,9 @@ public class DisplayDeviceConfig { loadDensityMapping(config); loadBrightnessDefaultFromDdcXml(config); loadBrightnessConstraintsFromConfigXml(); + if (mFlags.isEvenDimmerEnabled()) { + mLowBrightnessData = LowBrightnessData.loadConfig(config); + } loadBrightnessMap(config); loadThermalThrottlingConfig(config); loadPowerThrottlingConfigData(config); @@ -2549,9 +2639,9 @@ public class DisplayDeviceConfig { // A negative value means that there's no threshold mLowDisplayBrightnessThresholds[i] = thresholdNits; } else { - float thresholdBacklight = mNitsToBacklightSpline.interpolate(thresholdNits); + float thresholdBacklight = getBacklightFromNits(thresholdNits); mLowDisplayBrightnessThresholds[i] = - mBacklightToBrightnessSpline.interpolate(thresholdBacklight); + getBrightnessFromBacklight(thresholdBacklight); } mLowAmbientBrightnessThresholds[i] = lowerThresholdDisplayBrightnessPoints @@ -2600,9 +2690,9 @@ public class DisplayDeviceConfig { // A negative value means that there's no threshold mHighDisplayBrightnessThresholds[i] = thresholdNits; } else { - float thresholdBacklight = mNitsToBacklightSpline.interpolate(thresholdNits); + float thresholdBacklight = getBacklightFromNits(thresholdNits); mHighDisplayBrightnessThresholds[i] = - mBacklightToBrightnessSpline.interpolate(thresholdBacklight); + getBrightnessFromBacklight(thresholdBacklight); } mHighAmbientBrightnessThresholds[i] = higherThresholdDisplayBrightnessPoints @@ -2619,7 +2709,7 @@ public class DisplayDeviceConfig { loadAutoBrightnessBrighteningLightDebounceIdle(autoBrightness); loadAutoBrightnessDarkeningLightDebounceIdle(autoBrightness); mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags, - autoBrightness, mBacklightToBrightnessSpline); + autoBrightness, getBacklightToBrightnessSpline()); loadEnableAutoBrightness(autoBrightness); } @@ -2793,6 +2883,11 @@ public class DisplayDeviceConfig { // These splines are used to convert from the system brightness value to the HAL backlight // value private void createBacklightConversionSplines() { + + + // Create original brightness splines - not using low brightness mode arrays - this is + // so that we can continue to log the original brightness splines. + mBrightness = new float[mBacklight.length]; for (int i = 0; i < mBrightness.length; i++) { mBrightness[i] = MathUtils.map(mBacklight[0], @@ -2833,7 +2928,7 @@ public class DisplayDeviceConfig { + mBacklightMaximum); } mHbmData.transitionPoint = - mBacklightToBrightnessSpline.interpolate(transitionPointBacklightScale); + getBrightnessFromBacklight(transitionPointBacklightScale); final HbmTiming hbmTiming = hbm.getTiming_all(); mHbmData.timeWindowMillis = hbmTiming.getTimeWindowSecs_all().longValue() * 1000; mHbmData.timeMaxMillis = hbmTiming.getTimeMaxSecs_all().longValue() * 1000; @@ -2902,7 +2997,7 @@ public class DisplayDeviceConfig { continue; } luxToTransitionPointMap.put(lux, - mBacklightToBrightnessSpline.interpolate(maxBrightness)); + getBrightnessFromBacklight(maxBrightness)); } if (!luxToTransitionPointMap.isEmpty()) { mLuxThrottlingData.put(mappedType, luxToTransitionPointMap); @@ -2997,7 +3092,7 @@ public class DisplayDeviceConfig { private void loadAutoBrightnessConfigsFromConfigXml() { mDisplayBrightnessMapping = new DisplayBrightnessMappingConfig(mContext, mFlags, - /* autoBrightnessConfig= */ null, mBacklightToBrightnessSpline); + /* autoBrightnessConfig= */ null, getBacklightToBrightnessSpline()); } private void loadBrightnessChangeThresholdsFromXml() { @@ -3376,6 +3471,12 @@ public class DisplayDeviceConfig { throw new RuntimeException("Lux values should be in ascending order in the" + " idle screen refresh rate timeout config"); } + + int timeout = point.getTimeout().intValue(); + if (timeout < 0) { + throw new RuntimeException("The timeout value cannot be negative in" + + " idle screen refresh rate timeout config"); + } previousLux = newLux; } } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 87d017c978b1..90ad8c02c29c 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -1165,7 +1165,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call screenBrightnessThresholds, ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController, mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(), - mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits); + mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits, + mBrightnessClamperController); mDisplayBrightnessController.setAutomaticBrightnessController( mAutomaticBrightnessController); @@ -2479,6 +2480,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux, boolean slowChange) { mBrightnessRangeController.onAmbientLuxChange(ambientLux); + mBrightnessClamperController.onAmbientLuxChange(ambientLux); if (nits == BrightnessMappingStrategy.INVALID_NITS) { mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange); } else { @@ -3176,7 +3178,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call HysteresisLevels screenBrightnessThresholdsIdle, Context context, BrightnessRangeController brightnessModeController, BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort, - int ambientLightHorizonLong, float userLux, float userNits) { + int ambientLightHorizonLong, float userLux, float userNits, + BrightnessClamperController brightnessClamperController) { + return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor, brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate, @@ -3186,7 +3190,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call screenBrightnessThresholds, ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, context, brightnessModeController, brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux, - userNits); + userNits, brightnessClamperController); } BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context, diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index b2fd9edf61fe..86fab17e6ae8 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -37,6 +37,7 @@ import android.os.SystemProperties; import android.os.Trace; import android.util.DisplayUtils; import android.util.LongSparseArray; +import android.util.MathUtils; import android.util.Slog; import android.util.SparseArray; import android.view.Display; @@ -78,6 +79,13 @@ final class LocalDisplayAdapter extends DisplayAdapter { private static final String UNIQUE_ID_PREFIX = "local:"; private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.boot.emulator.circular"; + // Min and max strengths for even dimmer feature. + private static final float EVEN_DIMMER_MIN_STRENGTH = 0.0f; + private static final float EVEN_DIMMER_MAX_STRENGTH = 70.0f; // not too dim yet. + private static final float BRIGHTNESS_MIN = 0.0f; + // The brightness at which we start using color matrices rather than backlight, + // to dim the display + private static final float BACKLIGHT_COLOR_TRANSITION_POINT = 0.1f; private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>(); @@ -91,6 +99,8 @@ final class LocalDisplayAdapter extends DisplayAdapter { private Context mOverlayContext; + private int mEvenDimmerStrength = -1; + // Called with SyncRoot lock held. LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context, Handler handler, Listener listener, DisplayManagerFlags flags, @@ -928,6 +938,10 @@ final class LocalDisplayAdapter extends DisplayAdapter { final float nits = backlightToNits(backlight); final float sdrNits = backlightToNits(sdrBacklight); + if (getFeatureFlags().isEvenDimmerEnabled()) { + applyColorMatrixBasedDimming(brightnessState); + } + mBacklightAdapter.setBacklight(sdrBacklight, sdrNits, backlight, nits); Trace.traceCounter(Trace.TRACE_TAG_POWER, "ScreenBrightness", @@ -974,6 +988,22 @@ final class LocalDisplayAdapter extends DisplayAdapter { } } } + + private void applyColorMatrixBasedDimming(float brightnessState) { + int strength = (int) (MathUtils.constrainedMap( + EVEN_DIMMER_MAX_STRENGTH, EVEN_DIMMER_MIN_STRENGTH, // to this range + BRIGHTNESS_MIN, BACKLIGHT_COLOR_TRANSITION_POINT, // from this range + brightnessState) + 0.5); // map this (+ rounded up) + + if (mEvenDimmerStrength < 0 // uninitialised + || MathUtils.abs(mEvenDimmerStrength - strength) > 1 + || strength <= 1) { + mEvenDimmerStrength = strength; + } + + // TODO: use `enabled` and `mRbcStrength` to set color matrices here + // TODO: boolean enabled = mEvenDimmerStrength > 0.0f; + } }; } return null; @@ -1073,7 +1103,8 @@ final class LocalDisplayAdapter extends DisplayAdapter { new SurfaceControl.DesiredDisplayModeSpecs(baseSfModeId, mDisplayModeSpecs.allowGroupSwitching, mDisplayModeSpecs.primary, - mDisplayModeSpecs.appRequest))); + mDisplayModeSpecs.appRequest, + mDisplayModeSpecs.mIdleScreenRefreshRateConfig))); } } 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 18e8fab54e3e..d8a45009f236 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 @@ -189,6 +189,13 @@ public class BrightnessClamperController { mModifiers.forEach(BrightnessStateModifier::stop); } + /** + * Notifies modifiers that ambient lux has changed. + * @param ambientLux current lux, debounced + */ + public void onAmbientLuxChange(float ambientLux) { + mModifiers.forEach(modifier -> modifier.onAmbientLuxChange(ambientLux)); + } // Called in DisplayControllerHandler private void recalculateBrightnessCap() { diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java index 7f1f7a99e438..a91bb59b0bc0 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java @@ -39,21 +39,21 @@ import java.io.PrintWriter; * Class used to prevent the screen brightness dipping below a certain value, based on current * lux conditions and user preferred minimum. */ -public class BrightnessLowLuxModifier implements - BrightnessStateModifier { +public class BrightnessLowLuxModifier extends BrightnessModifier { // To enable these logs, run: // 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot' private static final String TAG = "BrightnessLowLuxModifier"; private static final boolean DEBUG = DebugUtils.isDebuggable(TAG); + private static final float MIN_NITS = 2.0f; private final SettingsObserver mSettingsObserver; private final ContentResolver mContentResolver; private final Handler mHandler; private final BrightnessClamperController.ClamperChangeListener mChangeListener; - protected float mSettingNitsLowerBound = PowerManager.BRIGHTNESS_MIN; private int mReason; private float mBrightnessLowerBound; private boolean mIsActive; + private float mAmbientLux; @VisibleForTesting BrightnessLowLuxModifier(Handler handler, @@ -78,17 +78,17 @@ public class BrightnessLowLuxModifier implements int userId = UserHandle.USER_CURRENT; float settingNitsLowerBound = Settings.Secure.getFloatForUser( mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS, - /* def= */ PowerManager.BRIGHTNESS_MIN, userId); + /* def= */ MIN_NITS, userId); - boolean isActive = Settings.Secure.getIntForUser(mContentResolver, + boolean isActive = Settings.Secure.getFloatForUser(mContentResolver, Settings.Secure.EVEN_DIMMER_ACTIVATED, - /* def= */ 0, userId) == 1; + /* def= */ 0, userId) == 1.0f; - // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux); - float luxBasedNitsLowerBound = 0.0f; + // TODO: luxBasedNitsLowerBound = mMinLuxToNitsSpline(currentLux); + float luxBasedNitsLowerBound = 2.0f; - // TODO: final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound, - // luxBasedNitsLowerBound) : PowerManager.BRIGHTNESS_MIN; + final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound, + luxBasedNitsLowerBound) : MIN_NITS; final int reason = settingNitsLowerBound > luxBasedNitsLowerBound ? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND @@ -104,8 +104,13 @@ public class BrightnessLowLuxModifier implements mReason = reason; if (DEBUG) { Slog.i(TAG, "isActive: " + isActive - + ", settingNitsLowerBound: " + settingNitsLowerBound - + ", lowerBound: " + brightnessLowerBound); + + ", brightnessLowerBound: " + brightnessLowerBound + + ", mAmbientLux: " + mAmbientLux + + ", mReason: " + ( + mReason == BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND ? "minSetting" + : "lux") + + ", nitsLowerBound: " + nitsLowerBound + ); } mBrightnessLowerBound = brightnessLowerBound; mChangeListener.onChanged(); @@ -132,6 +137,22 @@ public class BrightnessLowLuxModifier implements } @Override + boolean shouldApply(DisplayManagerInternal.DisplayPowerRequest request) { + return mIsActive; + } + + @Override + float getBrightnessAdjusted(float currentBrightness, + DisplayManagerInternal.DisplayPowerRequest request) { + return Math.max(mBrightnessLowerBound, currentBrightness); + } + + @Override + int getModifier() { + return mReason; + } + + @Override public void apply(DisplayManagerInternal.DisplayPowerRequest request, DisplayBrightnessState.Builder stateBuilder) { stateBuilder.setMinBrightness(mBrightnessLowerBound); @@ -150,10 +171,16 @@ public class BrightnessLowLuxModifier implements } @Override + public void onAmbientLuxChange(float ambientLux) { + mAmbientLux = ambientLux; + recalculateLowerBound(); + } + + @Override public void dump(PrintWriter pw) { pw.println("BrightnessLowLuxModifier:"); - pw.println(" mBrightnessLowerBound=" + mBrightnessLowerBound); pw.println(" mIsActive=" + mIsActive); + pw.println(" mBrightnessLowerBound=" + mBrightnessLowerBound); pw.println(" mReason=" + mReason); } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java index be8fa5a0f0ce..2a3dd8752615 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java @@ -68,4 +68,9 @@ abstract class BrightnessModifier implements BrightnessStateModifier { public void stop() { // do nothing } + + @Override + public void onAmbientLuxChange(float ambientLux) { + // do nothing + } } diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java index 441ba8f1a1fc..22342581fa8b 100644 --- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java +++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java @@ -42,4 +42,10 @@ public interface BrightnessStateModifier { * Called when stopped. Listeners can be unregistered here. */ void stop(); + + /** + * Allows modifiers to react to ambient lux changes. + * @param ambientLux current debounced lux. + */ + void onAmbientLuxChange(float ambientLux); } diff --git a/services/core/java/com/android/server/display/config/LowBrightnessData.java b/services/core/java/com/android/server/display/config/LowBrightnessData.java new file mode 100644 index 000000000000..aa82533bf6a7 --- /dev/null +++ b/services/core/java/com/android/server/display/config/LowBrightnessData.java @@ -0,0 +1,142 @@ +/* + * 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.server.display.config; + +import android.annotation.Nullable; +import android.util.Slog; +import android.util.Spline; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Arrays; +import java.util.List; + +/** + * Brightness config for low brightness mode + */ +public class LowBrightnessData { + private static final String TAG = "LowBrightnessData"; + + /** + * Brightness value at which lower brightness methods are used. + */ + public final float mTransitionPoint; + + /** + * Nits array, maps to mBacklight + */ + public final float[] mNits; + + /** + * Backlight array, maps to mBrightness and mNits + */ + public final float[] mBacklight; + + /** + * Brightness array, maps to mBacklight + */ + public final float[] mBrightness; + /** + * Spline, mapping between backlight and nits + */ + public final Spline mBacklightToNits; + /** + * Spline, mapping between nits and backlight + */ + public final Spline mNitsToBacklight; + /** + * Spline, mapping between brightness and backlight + */ + public final Spline mBrightnessToBacklight; + /** + * Spline, mapping between backlight and brightness + */ + public final Spline mBacklightToBrightness; + + @VisibleForTesting + public LowBrightnessData(float transitionPoint, float[] nits, + float[] backlight, float[] brightness, Spline backlightToNits, + Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness) { + mTransitionPoint = transitionPoint; + mNits = nits; + mBacklight = backlight; + mBrightness = brightness; + mBacklightToNits = backlightToNits; + mNitsToBacklight = nitsToBacklight; + mBrightnessToBacklight = brightnessToBacklight; + mBacklightToBrightness = backlightToBrightness; + } + + @Override + public String toString() { + return "LowBrightnessData {" + + "mTransitionPoint: " + mTransitionPoint + + ", mNits: " + Arrays.toString(mNits) + + ", mBacklight: " + Arrays.toString(mBacklight) + + ", mBrightness: " + Arrays.toString(mBrightness) + + ", mBacklightToNits: " + mBacklightToNits + + ", mNitsToBacklight: " + mNitsToBacklight + + ", mBrightnessToBacklight: " + mBrightnessToBacklight + + ", mBacklightToBrightness: " + mBacklightToBrightness + + "} "; + } + + /** + * Loads LowBrightnessData from DisplayConfiguration + */ + @Nullable + public static LowBrightnessData loadConfig(DisplayConfiguration config) { + final LowBrightnessMode lbm = config.getLowBrightness(); + if (lbm == null) { + return null; + } + + boolean lbmIsEnabled = lbm.getEnabled(); + if (!lbmIsEnabled) { + return null; + } + + List<Float> nitsList = lbm.getNits(); + List<Float> backlightList = lbm.getBacklight(); + List<Float> brightnessList = lbm.getBrightness(); + float transitionPoints = lbm.getTransitionPoint().floatValue(); + + if (nitsList.isEmpty() + || backlightList.size() != brightnessList.size() + || backlightList.size() != nitsList.size()) { + Slog.e(TAG, "Invalid low brightness array lengths"); + return null; + } + + float[] nits = new float[nitsList.size()]; + float[] backlight = new float[nitsList.size()]; + float[] brightness = new float[nitsList.size()]; + + for (int i = 0; i < nitsList.size(); i++) { + nits[i] = nitsList.get(i); + backlight[i] = backlightList.get(i); + brightness[i] = brightnessList.get(i); + } + + return new LowBrightnessData(transitionPoints, nits, backlight, brightness, + Spline.createSpline(backlight, nits), + Spline.createSpline(nits, backlight), + Spline.createSpline(brightness, backlight), + Spline.createSpline(backlight, brightness) + ); + } +} diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index 495ae87fe0b9..572d32e80c12 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -64,6 +64,8 @@ import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.view.Display; import android.view.DisplayInfo; +import android.view.SurfaceControl; +import android.view.SurfaceControl.IdleScreenRefreshRateConfig; import android.view.SurfaceControl.RefreshRateRange; import android.view.SurfaceControl.RefreshRateRanges; @@ -74,6 +76,7 @@ import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.os.BackgroundThread; import com.android.server.LocalServices; import com.android.server.display.DisplayDeviceConfig; +import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint; import com.android.server.display.feature.DeviceConfigParameterProvider; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.utils.AmbientFilter; @@ -184,6 +187,8 @@ public class DisplayModeDirector { private final boolean mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled; + private final DisplayManagerFlags mDisplayManagerFlags; + private final boolean mDvrrSupported; @@ -206,7 +211,7 @@ public class DisplayModeDirector { .isDisplaysRefreshRatesSynchronizationEnabled(); mIsBackUpSmoothDisplayAndForcePeakRefreshRateEnabled = displayManagerFlags .isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled(); - + mDisplayManagerFlags = displayManagerFlags; mContext = context; mHandler = new DisplayModeDirectorHandler(handler.getLooper()); mInjector = injector; @@ -374,7 +379,7 @@ public class DisplayModeDirector { final RefreshRateRanges ranges = new RefreshRateRanges(range, range); return new DesiredDisplayModeSpecs(defaultMode.getModeId(), /*allowGroupSwitching */ false, - ranges, ranges); + ranges, ranges, mBrightnessObserver.getIdleScreenRefreshRateConfig()); } boolean modeSwitchingDisabled = @@ -422,7 +427,8 @@ public class DisplayModeDirector { appRequestSummary.maxPhysicalRefreshRate), new RefreshRateRange( appRequestSummary.minRenderFrameRate, - appRequestSummary.maxRenderFrameRate))); + appRequestSummary.maxRenderFrameRate)), + mBrightnessObserver.getIdleScreenRefreshRateConfig()); } } @@ -764,6 +770,16 @@ public class DisplayModeDirector { public boolean allowGroupSwitching; /** + * Represents the idle time of the screen after which the associated display's refresh rate + * is to be reduced to preserve power + * Defaults to null, meaning that the device is not configured to have a timeout based on + * the surrounding conditions + * -1 means that the current conditions require no timeout + */ + @Nullable + public IdleScreenRefreshRateConfig mIdleScreenRefreshRateConfig; + + /** * The primary refresh rate ranges. */ public final RefreshRateRanges primary; @@ -783,11 +799,13 @@ public class DisplayModeDirector { public DesiredDisplayModeSpecs(int baseModeId, boolean allowGroupSwitching, @NonNull RefreshRateRanges primary, - @NonNull RefreshRateRanges appRequest) { + @NonNull RefreshRateRanges appRequest, + @Nullable SurfaceControl.IdleScreenRefreshRateConfig idleScreenRefreshRateConfig) { this.baseModeId = baseModeId; this.allowGroupSwitching = allowGroupSwitching; this.primary = primary; this.appRequest = appRequest; + this.mIdleScreenRefreshRateConfig = idleScreenRefreshRateConfig; } /** @@ -797,9 +815,10 @@ public class DisplayModeDirector { public String toString() { return String.format("baseModeId=%d allowGroupSwitching=%b" + " primary=%s" - + " appRequest=%s", + + " appRequest=%s" + + " idleScreenRefreshRateConfig=%s", baseModeId, allowGroupSwitching, primary.toString(), - appRequest.toString()); + appRequest.toString(), String.valueOf(mIdleScreenRefreshRateConfig)); } /** @@ -830,12 +849,18 @@ public class DisplayModeDirector { desiredDisplayModeSpecs.appRequest)) { return false; } + + if (!Objects.equals(mIdleScreenRefreshRateConfig, + desiredDisplayModeSpecs.mIdleScreenRefreshRateConfig)) { + return false; + } return true; } @Override public int hashCode() { - return Objects.hash(baseModeId, allowGroupSwitching, primary, appRequest); + return Objects.hash(baseModeId, allowGroupSwitching, primary, appRequest, + mIdleScreenRefreshRateConfig); } /** @@ -853,6 +878,14 @@ public class DisplayModeDirector { appRequest.physical.max = other.appRequest.physical.max; appRequest.render.min = other.appRequest.render.min; appRequest.render.max = other.appRequest.render.max; + + if (other.mIdleScreenRefreshRateConfig == null) { + mIdleScreenRefreshRateConfig = null; + } else { + mIdleScreenRefreshRateConfig = + new IdleScreenRefreshRateConfig( + other.mIdleScreenRefreshRateConfig.timeoutMillis); + } } } @@ -1543,12 +1576,20 @@ public class DisplayModeDirector { private float mAmbientLux = -1.0f; private AmbientFilter mAmbientFilter; + /** + * The current timeout configuration. This value is used by surface flinger to track the + * time after which an idle screen's refresh rate is to be reduced. + */ + @Nullable + private SurfaceControl.IdleScreenRefreshRateConfig mIdleScreenRefreshRateConfig; + private float mBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT; private final Context mContext; private final Injector mInjector; private final Handler mHandler; + private final boolean mVsyncLowLightBlockingVoteEnabled; private final IThermalEventListener.Stub mThermalListener = @@ -1643,6 +1684,11 @@ public class DisplayModeDirector { return mRefreshRateInLowZone; } + @VisibleForTesting + IdleScreenRefreshRateConfig getIdleScreenRefreshRateConfig() { + return mIdleScreenRefreshRateConfig; + } + private void loadLowBrightnessThresholds(@Nullable DisplayDeviceConfig displayDeviceConfig, boolean attemptReadFromFeatureParams) { loadRefreshRateInHighZone(displayDeviceConfig, attemptReadFromFeatureParams); @@ -2381,6 +2427,10 @@ public class DisplayModeDirector { // is interrupted by a new sensor event. mHandler.postDelayed(mInjectSensorEventRunnable, INJECT_EVENTS_INTERVAL_MS); } + + if (mDisplayManagerFlags.isIdleScreenRefreshRateTimeoutEnabled()) { + updateIdleScreenRefreshRate(mAmbientLux); + } } @Override @@ -2440,6 +2490,40 @@ public class DisplayModeDirector { } }; } + + private void updateIdleScreenRefreshRate(float ambientLux) { + List<IdleScreenRefreshRateTimeoutLuxThresholdPoint> + idleScreenRefreshRateTimeoutLuxThresholdPoints; + synchronized (mLock) { + if (mDefaultDisplayDeviceConfig == null || mDefaultDisplayDeviceConfig + .getIdleScreenRefreshRateTimeoutLuxThresholdPoint().isEmpty()) { + // Setting this to null will let surface flinger know that the idle timer is not + // configured in the display configs + mIdleScreenRefreshRateConfig = null; + return; + } + + idleScreenRefreshRateTimeoutLuxThresholdPoints = + mDefaultDisplayDeviceConfig + .getIdleScreenRefreshRateTimeoutLuxThresholdPoint(); + } + int newTimeout = -1; + for (IdleScreenRefreshRateTimeoutLuxThresholdPoint point : + idleScreenRefreshRateTimeoutLuxThresholdPoints) { + int newLux = point.getLux().intValue(); + if (newLux <= ambientLux) { + newTimeout = point.getTimeout().intValue(); + } + } + if (mIdleScreenRefreshRateConfig == null + || newTimeout != mIdleScreenRefreshRateConfig.timeoutMillis) { + mIdleScreenRefreshRateConfig = + new IdleScreenRefreshRateConfig(newTimeout); + synchronized (mLock) { + notifyDesiredDisplayModeSpecsChangedLocked(); + } + } + } } private class UdfpsObserver extends IUdfpsRefreshRateRequestCallback.Stub { diff --git a/services/core/java/com/android/server/feature/dropbox_flags.aconfig b/services/core/java/com/android/server/feature/dropbox_flags.aconfig index fee4bf377ddc..14e964b26c6b 100644 --- a/services/core/java/com/android/server/feature/dropbox_flags.aconfig +++ b/services/core/java/com/android/server/feature/dropbox_flags.aconfig @@ -2,6 +2,7 @@ package: "com.android.server.feature.flags" flag{ name: "enable_read_dropbox_permission" + is_exported: true namespace: "preload_safety" description: "Feature flag for permission to Read dropbox data" bug: "287512663" diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 05b1cb69235b..468b90259fc7 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -2604,6 +2604,19 @@ public class InputManagerService extends IInputManager.Stub mBatteryController.notifyStylusGestureStarted(deviceId, eventTime); } + // Native callback. + @SuppressWarnings("unused") + private int getPackageUid(String pkg) { + if (TextUtils.isEmpty(pkg)) { + return Process.INVALID_UID; + } + try { + return mContext.getPackageManager().getPackageUid(pkg, 0 /*flags*/); + } catch (PackageManager.NameNotFoundException e) { + return Process.INVALID_UID; + } + } + /** * Flatten a map into a string list, with value positioned directly next to the * key. diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index 283e692ffbab..661008103a25 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -459,13 +459,16 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { for (ResolveInfo resolveInfo : pm.queryBroadcastReceiversAsUser(intent, PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, UserHandle.USER_SYSTEM)) { + if (resolveInfo == null || resolveInfo.activityInfo == null) { + continue; + } final ActivityInfo activityInfo = resolveInfo.activityInfo; final int priority = resolveInfo.priority; visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor); } } - private void visitKeyboardLayout(String keyboardLayoutDescriptor, + private void visitKeyboardLayout(@NonNull String keyboardLayoutDescriptor, KeyboardLayoutVisitor visitor) { KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor); if (d != null) { @@ -482,8 +485,8 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } - private void visitKeyboardLayoutsInPackage(PackageManager pm, ActivityInfo receiver, - String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) { + private void visitKeyboardLayoutsInPackage(PackageManager pm, @NonNull ActivityInfo receiver, + @Nullable String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) { Bundle metaData = receiver.metaData; if (metaData == null) { return; @@ -1415,7 +1418,7 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { return packageName + "/" + receiverName + "/" + keyboardName; } - public static KeyboardLayoutDescriptor parse(String descriptor) { + public static KeyboardLayoutDescriptor parse(@NonNull String descriptor) { int pos = descriptor.indexOf('/'); if (pos < 0 || pos + 1 == descriptor.length()) { return null; diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java index 23fe5cca3d96..dbdac4184f28 100644 --- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java +++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java @@ -16,6 +16,8 @@ package com.android.server.inputmethod; +import static com.android.text.flags.Flags.handwritingEndOfLineTap; + import android.Manifest; import android.annotation.AnyThread; import android.annotation.NonNull; @@ -30,6 +32,7 @@ import android.hardware.input.InputManagerGlobal; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.SystemClock; import android.text.TextUtils; import android.util.Slog; import android.view.BatchedInputEventReceiver; @@ -66,6 +69,7 @@ final class HandwritingModeController { // Use getHandwritingBufferSize() and not this value directly. private static final int LONG_EVENT_BUFFER_SIZE = EVENT_BUFFER_SIZE * 20; private static final long HANDWRITING_DELEGATION_IDLE_TIMEOUT_MS = 3000; + private static final long AFTER_STYLUS_UP_ALLOW_PERIOD_MS = 200L; private final Context mContext; // This must be the looper for the UiThread. @@ -78,6 +82,7 @@ final class HandwritingModeController { private InputEventReceiver mHandwritingEventReceiver; private Runnable mInkWindowInitRunnable; private boolean mRecordingGesture; + private boolean mRecordingGestureAfterStylusUp; private int mCurrentDisplayId; // when set, package names are used for handwriting delegation. private @Nullable String mDelegatePackageName; @@ -155,6 +160,15 @@ final class HandwritingModeController { } boolean isStylusGestureOngoing() { + if (mRecordingGestureAfterStylusUp && !mHandwritingBuffer.isEmpty()) { + // If it is less than AFTER_STYLUS_UP_ALLOW_PERIOD_MS after the stylus up event, return + // true so that handwriting can start. + MotionEvent lastEvent = mHandwritingBuffer.get(mHandwritingBuffer.size() - 1); + if (lastEvent.getActionMasked() == MotionEvent.ACTION_UP) { + return SystemClock.uptimeMillis() - lastEvent.getEventTime() + < AFTER_STYLUS_UP_ALLOW_PERIOD_MS; + } + } return mRecordingGesture; } @@ -277,7 +291,7 @@ final class HandwritingModeController { Slog.e(TAG, "Cannot start handwriting session: Invalid request id: " + requestId); return null; } - if (!mRecordingGesture || mHandwritingBuffer.isEmpty()) { + if (!isStylusGestureOngoing()) { Slog.e(TAG, "Cannot start handwriting session: No stylus gesture is being recorded."); return null; } @@ -300,6 +314,7 @@ final class HandwritingModeController { mHandwritingEventReceiver.dispose(); mHandwritingEventReceiver = null; mRecordingGesture = false; + mRecordingGestureAfterStylusUp = false; if (mHandwritingSurface.isIntercepting()) { throw new IllegalStateException( @@ -362,6 +377,7 @@ final class HandwritingModeController { clearPendingHandwritingDelegation(); } mRecordingGesture = false; + mRecordingGestureAfterStylusUp = false; } private boolean onInputEvent(InputEvent ev) { @@ -412,15 +428,20 @@ final class HandwritingModeController { if ((TextUtils.isEmpty(mDelegatePackageName) || mDelegationConnectionlessFlow) && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) { mRecordingGesture = false; - mHandwritingBuffer.clear(); - return; + if (handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP) { + mRecordingGestureAfterStylusUp = true; + } else { + mHandwritingBuffer.clear(); + return; + } } if (action == MotionEvent.ACTION_DOWN) { + clearBufferIfRecordingAfterStylusUp(); mRecordingGesture = true; } - if (!mRecordingGesture) { + if (!mRecordingGesture && !mRecordingGestureAfterStylusUp) { return; } @@ -430,12 +451,20 @@ final class HandwritingModeController { + " The rest of the gesture will not be recorded."); } mRecordingGesture = false; + clearBufferIfRecordingAfterStylusUp(); return; } mHandwritingBuffer.add(MotionEvent.obtain(event)); } + private void clearBufferIfRecordingAfterStylusUp() { + if (mRecordingGestureAfterStylusUp) { + mHandwritingBuffer.clear(); + mRecordingGestureAfterStylusUp = false; + } + } + static final class HandwritingSession { private final int mRequestId; private final InputChannel mHandwritingChannel; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index d0a83a66dfba..cfd64c47718c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1248,7 +1248,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mService.publishLocalService(); IInputMethodManager.Stub service; if (Flags.useZeroJankProxy()) { - service = new ZeroJankProxy(mService.mHandler::post, mService); + service = + new ZeroJankProxy( + mService.mHandler::post, + mService, + () -> { + synchronized (ImfLock.class) { + return mService.isInputShown(); + } + }); } else { service = mService; } diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index 396192e085e7..31ce63056864 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -46,7 +46,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; -import android.util.ExceptionUtils; import android.util.Slog; import android.view.WindowManager; import android.view.inputmethod.CursorAnchorInfo; @@ -77,6 +76,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; /** * A proxy that processes all {@link IInputMethodManager} calls asynchronously. @@ -86,10 +86,12 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { private final IInputMethodManager mInner; private final Executor mExecutor; + private final BooleanSupplier mIsInputShown; - ZeroJankProxy(Executor executor, IInputMethodManager inner) { + ZeroJankProxy(Executor executor, IInputMethodManager inner, BooleanSupplier isInputShown) { mInner = inner; mExecutor = executor; + mIsInputShown = isInputShown; } private void offload(ThrowingRunnable r) { @@ -163,8 +165,19 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { int lastClickTooType, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) throws RemoteException { - offload(() -> mInner.showSoftInput(client, windowToken, statsToken, flags, lastClickTooType, - resultReceiver, reason)); + offload( + () -> { + if (!mInner.showSoftInput( + client, + windowToken, + statsToken, + flags, + lastClickTooType, + resultReceiver, + reason)) { + sendResultReceiverFailure(resultReceiver); + } + }); return true; } @@ -173,11 +186,24 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) throws RemoteException { - offload(() -> mInner.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver, - reason)); + offload( + () -> { + if (!mInner.hideSoftInput( + client, windowToken, statsToken, flags, resultReceiver, reason)) { + sendResultReceiverFailure(resultReceiver); + } + }); return true; } + private void sendResultReceiverFailure(ResultReceiver resultReceiver) { + resultReceiver.send( + mIsInputShown.getAsBoolean() + ? InputMethodManager.RESULT_UNCHANGED_SHOWN + : InputMethodManager.RESULT_UNCHANGED_HIDDEN, + null); + } + @Override @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) public void hideSoftInputFromServerForTest() throws RemoteException { @@ -415,14 +441,17 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { private void sendOnStartInputResult( IInputMethodClient client, InputBindResult res, int startInputSeq) { - InputMethodManagerService service = (InputMethodManagerService) mInner; - final ClientState cs = service.getClientState(client); - if (cs != null && cs.mClient != null) { - cs.mClient.onStartInputResult(res, startInputSeq); - } else { - // client is unbound. - Slog.i(TAG, "Client that requested startInputOrWindowGainedFocus is no longer" - + " bound. InputBindResult: " + res + " for startInputSeq: " + startInputSeq); + synchronized (ImfLock.class) { + InputMethodManagerService service = (InputMethodManagerService) mInner; + final ClientState cs = service.getClientState(client); + if (cs != null && cs.mClient != null) { + cs.mClient.onStartInputResult(res, startInputSeq); + } else { + // client is unbound. + Slog.i(TAG, "Client that requested startInputOrWindowGainedFocus is no longer" + + " bound. InputBindResult: " + res + " for startInputSeq: " + + startInputSeq); + } } } } diff --git a/services/core/java/com/android/server/location/LocationManagerService.java b/services/core/java/com/android/server/location/LocationManagerService.java index a608049cd677..6e991b4db2b1 100644 --- a/services/core/java/com/android/server/location/LocationManagerService.java +++ b/services/core/java/com/android/server/location/LocationManagerService.java @@ -17,6 +17,7 @@ package com.android.server.location; import static android.Manifest.permission.INTERACT_ACROSS_USERS; +import static android.Manifest.permission.LOCATION_BYPASS; import static android.Manifest.permission.WRITE_SECURE_SETTINGS; import static android.app.compat.CompatChanges.isChangeEnabled; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; @@ -34,6 +35,7 @@ import static android.location.provider.LocationProviderBase.ACTION_NETWORK_PROV import static com.android.server.location.LocationPermissions.PERMISSION_COARSE; import static com.android.server.location.LocationPermissions.PERMISSION_FINE; +import static com.android.server.location.LocationPermissions.PERMISSION_NONE; import static com.android.server.location.eventlog.LocationEventLog.EVENT_LOG; import static java.util.concurrent.TimeUnit.NANOSECONDS; @@ -73,6 +75,7 @@ import android.location.LocationManagerInternal.LocationPackageTagsListener; import android.location.LocationProvider; import android.location.LocationRequest; import android.location.LocationTime; +import android.location.flags.Flags; import android.location.provider.ForwardGeocodeRequest; import android.location.provider.IGeocodeCallback; import android.location.provider.IProviderRequestListener; @@ -776,8 +779,19 @@ public class LocationManagerService extends ILocationManager.Stub implements listenerId); int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(), identity.getPid()); - LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, - PERMISSION_COARSE); + if (Flags.enableLocationBypass()) { + if (permissionLevel == PERMISSION_NONE) { + if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) { + LocationPermissions.enforceLocationPermission( + identity.getUid(), permissionLevel, PERMISSION_COARSE); + } else { + permissionLevel = PERMISSION_FINE; + } + } + } else { + LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, + PERMISSION_COARSE); + } // clients in the system process must have an attribution tag set Preconditions.checkState(identity.getPid() != Process.myPid() || attributionTag != null); @@ -805,8 +819,19 @@ public class LocationManagerService extends ILocationManager.Stub implements listenerId); int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(), identity.getPid()); - LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, - PERMISSION_COARSE); + if (Flags.enableLocationBypass()) { + if (permissionLevel == PERMISSION_NONE) { + if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) { + LocationPermissions.enforceLocationPermission( + identity.getUid(), permissionLevel, PERMISSION_COARSE); + } else { + permissionLevel = PERMISSION_FINE; + } + } + } else { + LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, + PERMISSION_COARSE); + } // clients in the system process should have an attribution tag set if (identity.getPid() == Process.myPid() && attributionTag == null) { @@ -830,8 +855,19 @@ public class LocationManagerService extends ILocationManager.Stub implements AppOpsManager.toReceiverId(pendingIntent)); int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(), identity.getPid()); - LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, - PERMISSION_COARSE); + if (Flags.enableLocationBypass()) { + if (permissionLevel == PERMISSION_NONE) { + if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) { + LocationPermissions.enforceLocationPermission( + identity.getUid(), permissionLevel, PERMISSION_COARSE); + } else { + permissionLevel = PERMISSION_FINE; + } + } + } else { + LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, + PERMISSION_COARSE); + } // clients in the system process must have an attribution tag set Preconditions.checkArgument(identity.getPid() != Process.myPid() || attributionTag != null); @@ -982,8 +1018,19 @@ public class LocationManagerService extends ILocationManager.Stub implements CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag); int permissionLevel = LocationPermissions.getPermissionLevel(mContext, identity.getUid(), identity.getPid()); - LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, - PERMISSION_COARSE); + if (Flags.enableLocationBypass()) { + if (permissionLevel == PERMISSION_NONE) { + if (mContext.checkCallingPermission(LOCATION_BYPASS) != PERMISSION_GRANTED) { + LocationPermissions.enforceLocationPermission( + identity.getUid(), permissionLevel, PERMISSION_COARSE); + } else { + permissionLevel = PERMISSION_FINE; + } + } + } else { + LocationPermissions.enforceLocationPermission(identity.getUid(), permissionLevel, + PERMISSION_COARSE); + } // clients in the system process must have an attribution tag set Preconditions.checkArgument(identity.getPid() != Process.myPid() || attributionTag != null); diff --git a/services/core/java/com/android/server/location/provider/LocationProviderManager.java b/services/core/java/com/android/server/location/provider/LocationProviderManager.java index 40e538b02728..542a29ae4172 100644 --- a/services/core/java/com/android/server/location/provider/LocationProviderManager.java +++ b/services/core/java/com/android/server/location/provider/LocationProviderManager.java @@ -16,6 +16,7 @@ package com.android.server.location.provider; +import static android.Manifest.permission.LOCATION_BYPASS; import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION; import static android.app.AppOpsManager.OP_MONITOR_LOCATION; import static android.app.compat.CompatChanges.isChangeEnabled; @@ -51,6 +52,7 @@ import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.AlarmManager.OnAlarmListener; +import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.PendingIntent; import android.content.Context; @@ -66,6 +68,7 @@ import android.location.LocationRequest; import android.location.LocationResult; import android.location.LocationResult.BadLocationException; import android.location.altitude.AltitudeConverter; +import android.location.flags.Flags; import android.location.provider.IProviderRequestListener; import android.location.provider.ProviderProperties; import android.location.provider.ProviderRequest; @@ -106,6 +109,7 @@ import com.android.server.location.injector.AlarmHelper; import com.android.server.location.injector.AppForegroundHelper; import com.android.server.location.injector.AppForegroundHelper.AppForegroundListener; import com.android.server.location.injector.AppOpsHelper; +import com.android.server.location.injector.EmergencyHelper; import com.android.server.location.injector.Injector; import com.android.server.location.injector.LocationPermissionsHelper; import com.android.server.location.injector.LocationPermissionsHelper.LocationPermissionsListener; @@ -375,8 +379,13 @@ public class LocationProviderManager extends // we cache these values because checking/calculating on the fly is more expensive @GuardedBy("mMultiplexerLock") private boolean mPermitted; + + @GuardedBy("mMultiplexerLock") + private boolean mBypassPermitted; + @GuardedBy("mMultiplexerLock") private boolean mForeground; + @GuardedBy("mMultiplexerLock") private LocationRequest mProviderLocationRequest; @GuardedBy("mMultiplexerLock") @@ -421,8 +430,8 @@ public class LocationProviderManager extends EVENT_LOG.logProviderClientRegistered(mName, getIdentity(), mBaseRequest); // initialization order is important as there are ordering dependencies - mPermitted = mLocationPermissionsHelper.hasLocationPermissions(mPermissionLevel, - getIdentity()); + onLocationPermissionsChanged(); + onBypassLocationPermissionsChanged(mEmergencyHelper.isInEmergency(0)); mForeground = mAppForegroundHelper.isAppForeground(getIdentity().getUid()); mProviderLocationRequest = calculateProviderLocationRequest(); mIsUsingHighPower = isUsingHighPower(); @@ -491,7 +500,13 @@ public class LocationProviderManager extends public final boolean isPermitted() { synchronized (mMultiplexerLock) { - return mPermitted; + return mPermitted || mBypassPermitted; + } + } + + public final boolean isOnlyBypassPermitted() { + synchronized (mMultiplexerLock) { + return mBypassPermitted && !mPermitted; } } @@ -562,6 +577,33 @@ public class LocationProviderManager extends } } + boolean onBypassLocationPermissionsChanged(boolean isInEmergency) { + synchronized (mMultiplexerLock) { + boolean bypassPermitted = + Flags.enableLocationBypass() && isInEmergency + && mContext.checkPermission( + LOCATION_BYPASS, mIdentity.getPid(), mIdentity.getUid()) + == PERMISSION_GRANTED; + if (mBypassPermitted != bypassPermitted) { + if (D) { + Log.v( + TAG, + mName + + " provider package " + + getIdentity().getPackageName() + + " bypass permitted = " + + bypassPermitted); + } + + mBypassPermitted = bypassPermitted; + + return true; + } + + return false; + } + } + @GuardedBy("mMultiplexerLock") private boolean onLocationPermissionsChanged() { boolean permitted = mLocationPermissionsHelper.hasLocationPermissions(mPermissionLevel, @@ -941,8 +983,11 @@ public class LocationProviderManager extends } // note app ops - if (!mAppOpsHelper.noteOpNoThrow(LocationPermissions.asAppOp(getPermissionLevel()), - getIdentity())) { + int op = + Flags.enableLocationBypass() && isOnlyBypassPermitted() + ? AppOpsManager.OP_EMERGENCY_LOCATION + : LocationPermissions.asAppOp(getPermissionLevel()); + if (!mAppOpsHelper.noteOpNoThrow(op, getIdentity())) { if (D) { Log.w(TAG, mName + " provider registration " + getIdentity() + " noteOp denied"); @@ -1292,12 +1337,17 @@ public class LocationProviderManager extends } // lastly - note app ops - if (fineLocationResult != null && !mAppOpsHelper.noteOpNoThrow( - LocationPermissions.asAppOp(getPermissionLevel()), getIdentity())) { - if (D) { - Log.w(TAG, "noteOp denied for " + getIdentity()); + if (fineLocationResult != null) { + int op = + Flags.enableLocationBypass() && isOnlyBypassPermitted() + ? AppOpsManager.OP_EMERGENCY_LOCATION + : LocationPermissions.asAppOp(getPermissionLevel()); + if (!mAppOpsHelper.noteOpNoThrow(op, getIdentity())) { + if (D) { + Log.w(TAG, "noteOp denied for " + getIdentity()); + } + fineLocationResult = null; } - fineLocationResult = null; } if (fineLocationResult != null) { @@ -1399,6 +1449,7 @@ public class LocationProviderManager extends protected final ScreenInteractiveHelper mScreenInteractiveHelper; protected final LocationUsageLogger mLocationUsageLogger; protected final LocationFudger mLocationFudger; + protected final EmergencyHelper mEmergencyHelper; private final PackageResetHelper mPackageResetHelper; private final UserListener mUserChangedListener = this::onUserChanged; @@ -1434,6 +1485,8 @@ public class LocationProviderManager extends this::onLocationPowerSaveModeChanged; private final ScreenInteractiveChangedListener mScreenInteractiveChangedListener = this::onScreenInteractiveChanged; + private final EmergencyHelper.EmergencyStateChangedListener mEmergencyStateChangedListener = + this::onEmergencyStateChanged; private final PackageResetHelper.Responder mPackageResetResponder = new PackageResetHelper.Responder() { @Override @@ -1507,6 +1560,7 @@ public class LocationProviderManager extends mScreenInteractiveHelper = injector.getScreenInteractiveHelper(); mLocationUsageLogger = injector.getLocationUsageLogger(); mLocationFudger = new LocationFudger(mSettingsHelper.getCoarseLocationAccuracyM()); + mEmergencyHelper = injector.getEmergencyHelper(); mPackageResetHelper = injector.getPackageResetHelper(); mProvider = new MockableLocationProvider(mMultiplexerLock); @@ -1757,8 +1811,17 @@ public class LocationProviderManager extends if (location != null) { // lastly - note app ops - if (!mAppOpsHelper.noteOpNoThrow(LocationPermissions.asAppOp(permissionLevel), - identity)) { + int op = + (Flags.enableLocationBypass() + && !mLocationPermissionsHelper.hasLocationPermissions( + permissionLevel, identity) + && mEmergencyHelper.isInEmergency(0) + && mContext.checkPermission( + LOCATION_BYPASS, identity.getPid(), identity.getUid()) + == PERMISSION_GRANTED) + ? AppOpsManager.OP_EMERGENCY_LOCATION + : LocationPermissions.asAppOp(permissionLevel); + if (!mAppOpsHelper.noteOpNoThrow(op, identity)) { return null; } @@ -2069,6 +2132,9 @@ public class LocationProviderManager extends mAppForegroundHelper.addListener(mAppForegroundChangedListener); mLocationPowerSaveModeHelper.addListener(mLocationPowerSaveModeChangedListener); mScreenInteractiveHelper.addListener(mScreenInteractiveChangedListener); + if (Flags.enableLocationBypass()) { + mEmergencyHelper.addOnEmergencyStateChangedListener(mEmergencyStateChangedListener); + } mPackageResetHelper.register(mPackageResetResponder); } @@ -2088,6 +2154,9 @@ public class LocationProviderManager extends mAppForegroundHelper.removeListener(mAppForegroundChangedListener); mLocationPowerSaveModeHelper.removeListener(mLocationPowerSaveModeChangedListener); mScreenInteractiveHelper.removeListener(mScreenInteractiveChangedListener); + if (Flags.enableLocationBypass()) { + mEmergencyHelper.removeOnEmergencyStateChangedListener(mEmergencyStateChangedListener); + } mPackageResetHelper.unregister(mPackageResetResponder); } @@ -2466,6 +2535,12 @@ public class LocationProviderManager extends } } + private void onEmergencyStateChanged() { + boolean inEmergency = mEmergencyHelper.isInEmergency(0); + updateRegistrations( + registration -> registration.onBypassLocationPermissionsChanged(inEmergency)); + } + private void onBackgroundThrottlePackageWhitelistChanged() { updateRegistrations(Registration::onProviderLocationRequestChanged); } diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index a9a82725223d..5b3934ea9b13 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -687,27 +687,20 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde private static String toVolumeControlTypeString( @VolumeProvider.ControlType int volumeControlType) { - switch (volumeControlType) { - case VOLUME_CONTROL_FIXED: - return "FIXED"; - case VOLUME_CONTROL_RELATIVE: - return "RELATIVE"; - case VOLUME_CONTROL_ABSOLUTE: - return "ABSOLUTE"; - default: - return TextUtils.formatSimple("unknown(%d)", volumeControlType); - } + return switch (volumeControlType) { + case VOLUME_CONTROL_FIXED -> "FIXED"; + case VOLUME_CONTROL_RELATIVE -> "RELATIVE"; + case VOLUME_CONTROL_ABSOLUTE -> "ABSOLUTE"; + default -> TextUtils.formatSimple("unknown(%d)", volumeControlType); + }; } private static String toVolumeTypeString(@PlaybackInfo.PlaybackType int volumeType) { - switch (volumeType) { - case PLAYBACK_TYPE_LOCAL: - return "LOCAL"; - case PLAYBACK_TYPE_REMOTE: - return "REMOTE"; - default: - return TextUtils.formatSimple("unknown(%d)", volumeType); - } + return switch (volumeType) { + case PLAYBACK_TYPE_LOCAL -> "LOCAL"; + case PLAYBACK_TYPE_REMOTE -> "REMOTE"; + default -> TextUtils.formatSimple("unknown(%d)", volumeType); + }; } @Override diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index 18b495bfce5d..22f5332e150c 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -1214,16 +1214,14 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { return false; } final int previousProcState = previousInfo.procState; - if (mBackgroundNetworkRestricted && (previousProcState >= BACKGROUND_THRESHOLD_STATE) - != (newProcState >= BACKGROUND_THRESHOLD_STATE)) { - // Proc-state change crossed BACKGROUND_THRESHOLD_STATE: Network rules for the - // BACKGROUND chain may change. - return true; - } if ((previousProcState <= TOP_THRESHOLD_STATE) - != (newProcState <= TOP_THRESHOLD_STATE)) { - // Proc-state change crossed TOP_THRESHOLD_STATE: Network rules for the - // LOW_POWER_STANDBY chain may change. + || (newProcState <= TOP_THRESHOLD_STATE)) { + // If the proc-state change crossed TOP_THRESHOLD_STATE, network rules for the + // LOW_POWER_STANDBY chain may change, so we need to evaluate the transition. + // In addition, we always process changes when the new process state is + // TOP_THRESHOLD_STATE or below, to avoid situations where the TOP app ends up + // waiting for NPMS to finish processing newProcStateSeq, even when it was + // redundant (b/327303931). return true; } if ((previousProcState <= FOREGROUND_THRESHOLD_STATE) @@ -1232,6 +1230,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // different chains may change. return true; } + if (mBackgroundNetworkRestricted && (previousProcState >= BACKGROUND_THRESHOLD_STATE) + != (newProcState >= BACKGROUND_THRESHOLD_STATE)) { + // Proc-state change crossed BACKGROUND_THRESHOLD_STATE: Network rules for the + // BACKGROUND chain may change. + return true; + } final int networkCapabilities = PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK | PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK; if ((previousInfo.capability & networkCapabilities) @@ -4328,7 +4332,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @GuardedBy("mUidRulesFirstLock") private boolean updateUidStateUL(int uid, int procState, long procStateSeq, @ProcessCapability int capability) { - Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateUidStateUL"); + Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateUidStateUL: " + uid + "/" + + ActivityManager.procStateToString(procState) + "/" + procStateSeq + "/" + + ActivityManager.getCapabilitiesSummary(capability)); try { final UidState oldUidState = mUidState.get(uid); if (oldUidState != null && procStateSeq < oldUidState.procStateSeq) { diff --git a/services/core/java/com/android/server/net/OWNERS b/services/core/java/com/android/server/net/OWNERS index d0e95dd55b6c..669cdaaf3ab5 100644 --- a/services/core/java/com/android/server/net/OWNERS +++ b/services/core/java/com/android/server/net/OWNERS @@ -4,3 +4,4 @@ file:platform/packages/modules/Connectivity:main:/OWNERS_core_networking jsharkey@android.com sudheersai@google.com yamasani@google.com +suprabh@google.com diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index e80c79a8cffb..9fcdfdd564b6 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -11924,6 +11924,9 @@ public class NotificationManagerService extends SystemService { if (record != null && (record.getSbn().getNotification().flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) { boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND; + + // Lifetime extended notifications don't need to alert on state change. + record.setPostSilently(true); mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(), record, isAppForeground, mPostNotificationTrackerFactory.newTracker(null))); diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 4f3cdbc52259..50ca984dcf57 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -310,6 +310,7 @@ public class PreferencesHelper implements RankingConfig { parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY), parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE), bubblePref); + r.bubblePreference = bubblePref; r.priority = parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY); r.visibility = parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY); r.showBadge = parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE); @@ -676,7 +677,7 @@ public class PreferencesHelper implements RankingConfig { * @param bubblePreference whether bubbles are allowed. */ public void setBubblesAllowed(String pkg, int uid, int bubblePreference) { - boolean changed = false; + boolean changed; synchronized (mPackagePreferences) { PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid); changed = p.bubblePreference != bubblePreference; diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java index 28682e3d916f..953300ac43a6 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java @@ -37,8 +37,8 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.ServiceInfo; -import android.os.Binder; import android.content.res.Resources; +import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -163,7 +163,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } @Override - public void getVersion(RemoteCallback remoteCallback) throws RemoteException { + public void getVersion(RemoteCallback remoteCallback) { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion"); Objects.requireNonNull(remoteCallback); mContext.enforceCallingOrSelfPermission( @@ -244,7 +244,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { @Override public void requestFeatureDownload(Feature feature, - ICancellationSignal cancellationSignal, + AndroidFuture cancellationSignalFuture, IDownloadCallback downloadCallback) throws RemoteException { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestFeatureDownload"); Objects.requireNonNull(feature); @@ -261,16 +261,17 @@ public class OnDeviceIntelligenceManagerService extends SystemService { ensureRemoteIntelligenceServiceInitialized(); mRemoteOnDeviceIntelligenceService.run( service -> service.requestFeatureDownload(Binder.getCallingUid(), feature, - cancellationSignal, + cancellationSignalFuture, downloadCallback)); } @Override public void requestTokenInfo(Feature feature, - Bundle request, ICancellationSignal cancellationSignal, + Bundle request, + AndroidFuture cancellationSignalFuture, ITokenInfoCallback tokenInfoCallback) throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal prepareFeatureProcessing"); + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestTokenInfo"); Objects.requireNonNull(feature); Objects.requireNonNull(request); Objects.requireNonNull(tokenInfoCallback); @@ -285,10 +286,11 @@ public class OnDeviceIntelligenceManagerService extends SystemService { PersistableBundle.EMPTY); } ensureRemoteInferenceServiceInitialized(); + mRemoteInferenceService.run( service -> service.requestTokenInfo(Binder.getCallingUid(), feature, request, - cancellationSignal, + cancellationSignalFuture, tokenInfoCallback)); } @@ -296,8 +298,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void processRequest(Feature feature, Bundle request, int requestType, - ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IResponseCallback responseCallback) throws RemoteException { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest"); @@ -316,7 +318,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { mRemoteInferenceService.run( service -> service.processRequest(Binder.getCallingUid(), feature, request, requestType, - cancellationSignal, processingSignal, + cancellationSignalFuture, processingSignalFuture, responseCallback)); } @@ -324,8 +326,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void processRequestStreaming(Feature feature, Bundle request, int requestType, - ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IStreamingResponseCallback streamingCallback) throws RemoteException { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming"); Objects.requireNonNull(feature); @@ -343,7 +345,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { mRemoteInferenceService.run( service -> service.processRequestStreaming(Binder.getCallingUid(), feature, request, requestType, - cancellationSignal, processingSignal, + cancellationSignalFuture, processingSignalFuture, streamingCallback)); } @@ -356,11 +358,11 @@ public class OnDeviceIntelligenceManagerService extends SystemService { }; } - private void ensureRemoteIntelligenceServiceInitialized() throws RemoteException { + private void ensureRemoteIntelligenceServiceInitialized() { synchronized (mLock) { if (mRemoteOnDeviceIntelligenceService == null) { String serviceName = getServiceNames()[0]; - validateService(serviceName, false); + Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, false)); mRemoteOnDeviceIntelligenceService = new RemoteOnDeviceIntelligenceService(mContext, ComponentName.unflattenFromString(serviceName), UserHandle.SYSTEM.getIdentifier()); @@ -388,29 +390,19 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void updateProcessingState( Bundle processingState, IProcessingUpdateStatusCallback callback) { - try { - ensureRemoteInferenceServiceInitialized(); - mRemoteInferenceService.run( - service -> service.updateProcessingState( - processingState, callback)); - } catch (RemoteException unused) { - try { - callback.onFailure( - OnDeviceIntelligenceException.PROCESSING_UPDATE_STATUS_CONNECTION_FAILED, - "Received failure invoking the remote processing service."); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to send failure status.", ex); - } - } + ensureRemoteInferenceServiceInitialized(); + mRemoteInferenceService.run( + service -> service.updateProcessingState( + processingState, callback)); } }; } - private void ensureRemoteInferenceServiceInitialized() throws RemoteException { + private void ensureRemoteInferenceServiceInitialized() { synchronized (mLock) { if (mRemoteInferenceService == null) { String serviceName = getServiceNames()[1]; - validateService(serviceName, true); + Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, true)); mRemoteInferenceService = new RemoteOnDeviceSandboxedInferenceService(mContext, ComponentName.unflattenFromString(serviceName), UserHandle.SYSTEM.getIdentifier()); @@ -457,35 +449,38 @@ public class OnDeviceIntelligenceManagerService extends SystemService { }; } - @GuardedBy("mLock") - private void validateService(String serviceName, boolean checkIsolated) - throws RemoteException { - if (TextUtils.isEmpty(serviceName)) { - throw new RuntimeException(""); - } - ComponentName serviceComponent = ComponentName.unflattenFromString( - serviceName); - ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( - serviceComponent, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0); - if (serviceInfo != null) { - if (!checkIsolated) { - checkServiceRequiresPermission(serviceInfo, - Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE); - return; + private void validateServiceElevated(String serviceName, boolean checkIsolated) { + try { + if (TextUtils.isEmpty(serviceName)) { + throw new IllegalStateException( + "Remote service is not configured to complete the request"); } + ComponentName serviceComponent = ComponentName.unflattenFromString( + serviceName); + ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( + serviceComponent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0); + if (serviceInfo != null) { + if (!checkIsolated) { + checkServiceRequiresPermission(serviceInfo, + Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE); + return; + } - checkServiceRequiresPermission(serviceInfo, - Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE); - if (!isIsolatedService(serviceInfo)) { - throw new SecurityException( - "Call required an isolated service, but the configured service: " - + serviceName + ", is not isolated"); + checkServiceRequiresPermission(serviceInfo, + Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE); + if (!isIsolatedService(serviceInfo)) { + throw new SecurityException( + "Call required an isolated service, but the configured service: " + + serviceName + ", is not isolated"); + } + } else { + throw new IllegalStateException( + "Remote service is not configured to complete the request."); } - } else { - throw new RuntimeException( - "Could not find service info for serviceName: " + serviceName); + } catch (RemoteException e) { + throw new IllegalStateException("Could not fetch service info for remote services", e); } } @@ -501,8 +496,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } } - @GuardedBy("mLock") - private boolean isIsolatedService(@NonNull ServiceInfo serviceInfo) { + private static boolean isIsolatedService(@NonNull ServiceInfo serviceInfo) { return (serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0 && (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0; } @@ -544,7 +538,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); synchronized (mLock) { mTemporaryServiceNames = componentNames; - + mRemoteOnDeviceIntelligenceService = null; + mRemoteInferenceService = null; if (mTemporaryHandler == null) { mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { @Override diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java index 18ba2cf1405e..9ba88aa18ce6 100644 --- a/services/core/java/com/android/server/pm/AppDataHelper.java +++ b/services/core/java/com/android/server/pm/AppDataHelper.java @@ -45,7 +45,6 @@ import android.util.TimingsTraceLog; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; import com.android.server.SystemServerInitThreadPool; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.parsing.pkg.AndroidPackageUtils; import com.android.server.pm.pkg.AndroidPackage; @@ -256,41 +255,6 @@ public class AppDataHelper { } } - if (!DexOptHelper.useArtService()) { // ART Service handles this on demand instead. - // Prepare the application profiles only for upgrades and - // first boot (so that we don't repeat the same operation at - // each boot). - // - // We only have to cover the upgrade and first boot here - // because for app installs we prepare the profiles before - // invoking dexopt (in installPackageLI). - // - // We also have to cover non system users because we do not - // call the usual install package methods for them. - // - // NOTE: in order to speed up first boot time we only create - // the current profile and do not update the content of the - // reference profile. A system image should already be - // configured with the right profile keys and the profiles - // for the speed-profile prebuilds should already be copied. - // That's done in #performDexOptUpgrade. - // - // TODO(calin, mathieuc): We should use .dm files for - // prebuilds profiles instead of manually copying them in - // #performDexOptUpgrade. When we do that we should have a - // more granular check here and only update the existing - // profiles. - if (pkg != null && (mPm.isDeviceUpgrading() || mPm.isFirstBoot() - || (userId != UserHandle.USER_SYSTEM))) { - try { - mArtManagerService.prepareAppProfiles(pkg, userId, - /* updateReferenceProfileContent= */ false); - } catch (LegacyDexoptDisabledException e2) { - throw new RuntimeException(e2); - } - } - } - final long ceDataInode = createAppDataResult.ceDataInode; final long deDataInode = createAppDataResult.deDataInode; @@ -615,15 +579,7 @@ public class AppDataHelper { Slog.wtf(TAG, "Package was null!", new Throwable()); return; } - if (DexOptHelper.useArtService()) { - destroyAppProfilesWithArtService(pkg.getPackageName()); - } else { - try { - mArtManagerService.clearAppProfiles(pkg); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - } + destroyAppProfilesLIF(pkg.getPackageName()); } public void destroyAppDataLIF(AndroidPackage pkg, int userId, int flags) { @@ -657,20 +613,6 @@ public class AppDataHelper { * Destroy ART app profiles for the package. */ void destroyAppProfilesLIF(String packageName) { - if (DexOptHelper.useArtService()) { - destroyAppProfilesWithArtService(packageName); - } else { - try { - mInstaller.destroyAppProfiles(packageName); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } catch (Installer.InstallerException e) { - Slog.w(TAG, String.valueOf(e)); - } - } - } - - private void destroyAppProfilesWithArtService(String packageName) { if (!DexOptHelper.artManagerLocalIsInitialized()) { // This function may get called while PackageManagerService is constructed (via e.g. // InitAppsHelper.initSystemApps), and ART Service hasn't yet been started then (it diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptJobService.java b/services/core/java/com/android/server/pm/BackgroundDexOptJobService.java deleted file mode 100644 index d9452742f99c..000000000000 --- a/services/core/java/com/android/server/pm/BackgroundDexOptJobService.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.pm; - -import android.app.job.JobParameters; -import android.app.job.JobService; - -/** - * JobService to run background dex optimization. This is a thin wrapper and most logic exits in - * {@link BackgroundDexOptService}. - */ -public final class BackgroundDexOptJobService extends JobService { - - @Override - public boolean onStartJob(JobParameters params) { - return BackgroundDexOptService.getService().onStartJob(this, params); - } - - @Override - public boolean onStopJob(JobParameters params) { - return BackgroundDexOptService.getService().onStopJob(this, params); - } -} diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java deleted file mode 100644 index 36677df07ca3..000000000000 --- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java +++ /dev/null @@ -1,1152 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.pm; - -import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; -import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason; -import static com.android.server.pm.dex.ArtStatsLogUtils.BackgroundDexoptJobStatsLogger; - -import static dalvik.system.DexFile.isProfileGuidedCompilerFilter; - -import android.annotation.IntDef; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.job.JobInfo; -import android.app.job.JobParameters; -import android.app.job.JobScheduler; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageInfo; -import android.os.BatteryManagerInternal; -import android.os.Binder; -import android.os.Environment; -import android.os.IThermalService; -import android.os.PowerManager; -import android.os.Process; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.SystemProperties; -import android.os.Trace; -import android.os.UserHandle; -import android.os.storage.StorageManager; -import android.util.ArraySet; -import android.util.Log; -import android.util.Slog; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.FrameworkStatsLog; -import com.android.internal.util.FunctionalUtils.ThrowingCheckedSupplier; -import com.android.internal.util.IndentingPrintWriter; -import com.android.server.LocalServices; -import com.android.server.PinnerService; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; -import com.android.server.pm.PackageDexOptimizer.DexOptResult; -import com.android.server.pm.dex.DexManager; -import com.android.server.pm.dex.DexoptOptions; -import com.android.server.utils.TimingsTraceAndSlog; - -import java.io.File; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Controls background dex optimization run as idle job or command line. - */ -public final class BackgroundDexOptService { - private static final String TAG = "BackgroundDexOptService"; - - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - @VisibleForTesting static final int JOB_IDLE_OPTIMIZE = 800; - @VisibleForTesting static final int JOB_POST_BOOT_UPDATE = 801; - - private static final long IDLE_OPTIMIZATION_PERIOD = TimeUnit.DAYS.toMillis(1); - - private static final long CANCELLATION_WAIT_CHECK_INTERVAL_MS = 200; - - private static final ComponentName sDexoptServiceName = - new ComponentName("android", BackgroundDexOptJobService.class.getName()); - - // Possible return codes of individual optimization steps. - /** Initial value. */ - public static final int STATUS_UNSPECIFIED = -1; - /** Ok status: Optimizations finished, All packages were processed, can continue */ - public static final int STATUS_OK = 0; - /** Optimizations should be aborted. Job scheduler requested it. */ - public static final int STATUS_ABORT_BY_CANCELLATION = 1; - /** Optimizations should be aborted. No space left on device. */ - public static final int STATUS_ABORT_NO_SPACE_LEFT = 2; - /** Optimizations should be aborted. Thermal throttling level too high. */ - public static final int STATUS_ABORT_THERMAL = 3; - /** Battery level too low */ - public static final int STATUS_ABORT_BATTERY = 4; - /** - * {@link PackageDexOptimizer#DEX_OPT_FAILED} case. This state means some packages have failed - * compilation during the job. Note that the failure will not be permanent as the next dexopt - * job will exclude those failed packages. - */ - public static final int STATUS_DEX_OPT_FAILED = 5; - /** Encountered fatal error, such as a runtime exception. */ - public static final int STATUS_FATAL_ERROR = 6; - - @IntDef(prefix = {"STATUS_"}, - value = - { - STATUS_UNSPECIFIED, - STATUS_OK, - STATUS_ABORT_BY_CANCELLATION, - STATUS_ABORT_NO_SPACE_LEFT, - STATUS_ABORT_THERMAL, - STATUS_ABORT_BATTERY, - STATUS_DEX_OPT_FAILED, - STATUS_FATAL_ERROR, - }) - @Retention(RetentionPolicy.SOURCE) - public @interface Status {} - - // Used for calculating space threshold for downgrading unused apps. - private static final int LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE = 2; - - // Thermal cutoff value used if one isn't defined by a system property. - private static final int THERMAL_CUTOFF_DEFAULT = PowerManager.THERMAL_STATUS_MODERATE; - - private final Injector mInjector; - - private final DexOptHelper mDexOptHelper; - - private final BackgroundDexoptJobStatsLogger mStatsLogger = - new BackgroundDexoptJobStatsLogger(); - - private final Object mLock = new Object(); - - // Thread currently running dexopt. This will be null if dexopt is not running. - // The thread running dexopt make sure to set this into null when the pending dexopt is - // completed. - @GuardedBy("mLock") @Nullable private Thread mDexOptThread; - - // Thread currently cancelling dexopt. This thread is in blocked wait state until - // cancellation is done. Only this thread can change states for control. The other threads, if - // need to wait for cancellation, should just wait without doing any control. - @GuardedBy("mLock") @Nullable private Thread mDexOptCancellingThread; - - // Tells whether post boot update is completed or not. - @GuardedBy("mLock") private boolean mFinishedPostBootUpdate; - - // True if JobScheduler invocations of dexopt have been disabled. - @GuardedBy("mLock") private boolean mDisableJobSchedulerJobs; - - @GuardedBy("mLock") @Status private int mLastExecutionStatus = STATUS_UNSPECIFIED; - - @GuardedBy("mLock") private long mLastExecutionStartUptimeMs; - @GuardedBy("mLock") private long mLastExecutionDurationMs; - - // Keeps packages cancelled from PDO for last session. This is for debugging. - @GuardedBy("mLock") - private final ArraySet<String> mLastCancelledPackages = new ArraySet<String>(); - - /** - * Set of failed packages remembered across job runs. - */ - @GuardedBy("mLock") - private final ArraySet<String> mFailedPackageNamesPrimary = new ArraySet<String>(); - @GuardedBy("mLock") - private final ArraySet<String> mFailedPackageNamesSecondary = new ArraySet<String>(); - - private final long mDowngradeUnusedAppsThresholdInMillis; - - private final List<PackagesUpdatedListener> mPackagesUpdatedListeners = new ArrayList<>(); - - private int mThermalStatusCutoff = THERMAL_CUTOFF_DEFAULT; - - /** Listener for monitoring package change due to dexopt. */ - public interface PackagesUpdatedListener { - /** Called when the packages are updated through dexopt */ - void onPackagesUpdated(ArraySet<String> updatedPackages); - } - - public BackgroundDexOptService(Context context, DexManager dexManager, PackageManagerService pm) - throws LegacyDexoptDisabledException { - this(new Injector(context, dexManager, pm)); - } - - @VisibleForTesting - public BackgroundDexOptService(Injector injector) throws LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); - mInjector = injector; - mDexOptHelper = mInjector.getDexOptHelper(); - LocalServices.addService(BackgroundDexOptService.class, this); - mDowngradeUnusedAppsThresholdInMillis = mInjector.getDowngradeUnusedAppsThresholdInMillis(); - } - - /** Start scheduling job after boot completion */ - public void systemReady() throws LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); - if (mInjector.isBackgroundDexOptDisabled()) { - return; - } - - mInjector.getContext().registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - mInjector.getContext().unregisterReceiver(this); - // queue both job. JOB_IDLE_OPTIMIZE will not start until JOB_POST_BOOT_UPDATE is - // completed. - scheduleAJob(JOB_POST_BOOT_UPDATE); - scheduleAJob(JOB_IDLE_OPTIMIZE); - if (DEBUG) { - Slog.d(TAG, "BootBgDexopt scheduled"); - } - } - }, new IntentFilter(Intent.ACTION_BOOT_COMPLETED)); - } - - /** Dump the current state */ - public void dump(IndentingPrintWriter writer) { - boolean disabled = mInjector.isBackgroundDexOptDisabled(); - writer.print("enabled:"); - writer.println(!disabled); - if (disabled) { - return; - } - synchronized (mLock) { - writer.print("mDexOptThread:"); - writer.println(mDexOptThread); - writer.print("mDexOptCancellingThread:"); - writer.println(mDexOptCancellingThread); - writer.print("mFinishedPostBootUpdate:"); - writer.println(mFinishedPostBootUpdate); - writer.print("mDisableJobSchedulerJobs:"); - writer.println(mDisableJobSchedulerJobs); - writer.print("mLastExecutionStatus:"); - writer.println(mLastExecutionStatus); - writer.print("mLastExecutionStartUptimeMs:"); - writer.println(mLastExecutionStartUptimeMs); - writer.print("mLastExecutionDurationMs:"); - writer.println(mLastExecutionDurationMs); - writer.print("now:"); - writer.println(SystemClock.elapsedRealtime()); - writer.print("mLastCancelledPackages:"); - writer.println(String.join(",", mLastCancelledPackages)); - writer.print("mFailedPackageNamesPrimary:"); - writer.println(String.join(",", mFailedPackageNamesPrimary)); - writer.print("mFailedPackageNamesSecondary:"); - writer.println(String.join(",", mFailedPackageNamesSecondary)); - } - } - - /** Gets the instance of the service */ - public static BackgroundDexOptService getService() { - return LocalServices.getService(BackgroundDexOptService.class); - } - - /** - * Executes the background dexopt job immediately for selected packages or all packages. - * - * <p>This is only for shell command and only root or shell user can use this. - * - * @param packageNames dex optimize the passed packages in the given order, or all packages in - * the default order if null - * - * @return true if dex optimization is complete. false if the task is cancelled or if there was - * an error. - */ - public boolean runBackgroundDexoptJob(@Nullable List<String> packageNames) - throws LegacyDexoptDisabledException { - enforceRootOrShell(); - long identity = Binder.clearCallingIdentity(); - try { - synchronized (mLock) { - // Do not cancel and wait for completion if there is pending task. - waitForDexOptThreadToFinishLocked(); - resetStatesForNewDexOptRunLocked(Thread.currentThread()); - } - PackageManagerService pm = mInjector.getPackageManagerService(); - List<String> packagesToOptimize; - if (packageNames == null) { - packagesToOptimize = mDexOptHelper.getOptimizablePackages(pm.snapshotComputer()); - } else { - packagesToOptimize = packageNames; - } - return runIdleOptimization(pm, packagesToOptimize, /* isPostBootUpdate= */ false); - } finally { - Binder.restoreCallingIdentity(identity); - markDexOptCompleted(); - } - } - - /** - * Cancels currently running any idle optimization tasks started from JobScheduler - * or runIdleOptimization call. - * - * <p>This is only for shell command and only root or shell user can use this. - */ - public void cancelBackgroundDexoptJob() throws LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); - enforceRootOrShell(); - Binder.withCleanCallingIdentity(() -> cancelDexOptAndWaitForCompletion()); - } - - /** - * Sets a flag that disables jobs from being started from JobScheduler. - * - * This state is not persistent and is only retained in this service instance. - * - * This is intended for shell command use and only root or shell users can call it. - * - * @param disable True if JobScheduler invocations should be disabled, false otherwise. - */ - public void setDisableJobSchedulerJobs(boolean disable) throws LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); - enforceRootOrShell(); - synchronized (mLock) { - mDisableJobSchedulerJobs = disable; - } - } - - /** Adds listener for package update */ - public void addPackagesUpdatedListener(PackagesUpdatedListener listener) - throws LegacyDexoptDisabledException { - // TODO(b/251903639): Evaluate whether this needs to support ART Service or not. - Installer.checkLegacyDexoptDisabled(); - synchronized (mLock) { - mPackagesUpdatedListeners.add(listener); - } - } - - /** Removes package update listener */ - public void removePackagesUpdatedListener(PackagesUpdatedListener listener) - throws LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); - synchronized (mLock) { - mPackagesUpdatedListeners.remove(listener); - } - } - - /** - * Notifies package change and removes the package from the failed package list so that - * the package can run dexopt again. - */ - public void notifyPackageChanged(String packageName) throws LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); - // The idle maintenance job skips packages which previously failed to - // compile. The given package has changed and may successfully compile - // now. Remove it from the list of known failing packages. - synchronized (mLock) { - mFailedPackageNamesPrimary.remove(packageName); - mFailedPackageNamesSecondary.remove(packageName); - } - } - - /** For BackgroundDexOptJobService to dispatch onStartJob event */ - /* package */ boolean onStartJob(BackgroundDexOptJobService job, JobParameters params) { - Slog.i(TAG, "onStartJob:" + params.getJobId()); - - boolean isPostBootUpdateJob = params.getJobId() == JOB_POST_BOOT_UPDATE; - // NOTE: PackageManagerService.isStorageLow uses a different set of criteria from - // the checks above. This check is not "live" - the value is determined by a background - // restart with a period of ~1 minute. - PackageManagerService pm = mInjector.getPackageManagerService(); - if (pm.isStorageLow()) { - Slog.w(TAG, "Low storage, skipping this run"); - markPostBootUpdateCompleted(params); - return false; - } - - List<String> pkgs = mDexOptHelper.getOptimizablePackages(pm.snapshotComputer()); - if (pkgs.isEmpty()) { - Slog.i(TAG, "No packages to optimize"); - markPostBootUpdateCompleted(params); - return false; - } - - mThermalStatusCutoff = mInjector.getDexOptThermalCutoff(); - - synchronized (mLock) { - if (mDisableJobSchedulerJobs) { - Slog.i(TAG, "JobScheduler invocations disabled"); - return false; - } - if (mDexOptThread != null && mDexOptThread.isAlive()) { - // Other task is already running. - return false; - } - if (!isPostBootUpdateJob && !mFinishedPostBootUpdate) { - // Post boot job not finished yet. Run post boot job first. - return false; - } - try { - resetStatesForNewDexOptRunLocked(mInjector.createAndStartThread( - "BackgroundDexOptService_" + (isPostBootUpdateJob ? "PostBoot" : "Idle"), - () -> { - TimingsTraceAndSlog tr = - new TimingsTraceAndSlog(TAG, Trace.TRACE_TAG_DALVIK); - tr.traceBegin("jobExecution"); - boolean completed = false; - boolean fatalError = false; - try { - completed = runIdleOptimization( - pm, pkgs, params.getJobId() == JOB_POST_BOOT_UPDATE); - } catch (LegacyDexoptDisabledException e) { - Slog.wtf(TAG, e); - } catch (RuntimeException e) { - fatalError = true; - throw e; - } finally { // Those cleanup should be done always. - tr.traceEnd(); - Slog.i(TAG, - "dexopt finishing. jobid:" + params.getJobId() - + " completed:" + completed); - - writeStatsLog(params); - - if (params.getJobId() == JOB_POST_BOOT_UPDATE) { - if (completed) { - markPostBootUpdateCompleted(params); - } - } - // Reschedule when cancelled. No need to reschedule when failed with - // fatal error because it's likely to fail again. - job.jobFinished(params, !completed && !fatalError); - markDexOptCompleted(); - } - })); - } catch (LegacyDexoptDisabledException e) { - Slog.wtf(TAG, e); - } - } - return true; - } - - /** For BackgroundDexOptJobService to dispatch onStopJob event */ - /* package */ boolean onStopJob(BackgroundDexOptJobService job, JobParameters params) { - Slog.i(TAG, "onStopJob:" + params.getJobId()); - // This cannot block as it is in main thread, thus dispatch to a newly created thread - // and cancel it from there. As this event does not happen often, creating a new thread - // is justified rather than having one thread kept permanently. - mInjector.createAndStartThread("DexOptCancel", () -> { - try { - cancelDexOptAndWaitForCompletion(); - } catch (LegacyDexoptDisabledException e) { - Slog.wtf(TAG, e); - } - }); - // Always reschedule for cancellation. - return true; - } - - /** - * Cancels pending dexopt and wait for completion of the cancellation. This can block the caller - * until cancellation is done. - */ - private void cancelDexOptAndWaitForCompletion() throws LegacyDexoptDisabledException { - synchronized (mLock) { - if (mDexOptThread == null) { - return; - } - if (mDexOptCancellingThread != null && mDexOptCancellingThread.isAlive()) { - // No control, just wait - waitForDexOptThreadToFinishLocked(); - // Do not wait for other cancellation's complete. That will be handled by the next - // start flow. - return; - } - mDexOptCancellingThread = Thread.currentThread(); - // Take additional caution to make sure that we do not leave this call - // with controlDexOptBlockingLocked(true) state. - try { - controlDexOptBlockingLocked(true); - waitForDexOptThreadToFinishLocked(); - } finally { - // Reset to default states regardless of previous states - mDexOptCancellingThread = null; - mDexOptThread = null; - controlDexOptBlockingLocked(false); - mLock.notifyAll(); - } - } - } - - @GuardedBy("mLock") - private void waitForDexOptThreadToFinishLocked() { - TimingsTraceAndSlog tr = new TimingsTraceAndSlog(TAG, Trace.TRACE_TAG_PACKAGE_MANAGER); - // This tracing section doesn't have any correspondence in ART Service - it never waits for - // cancellation to finish. - tr.traceBegin("waitForDexOptThreadToFinishLocked"); - try { - // Wait but check in regular internal to see if the thread is still alive. - while (mDexOptThread != null && mDexOptThread.isAlive()) { - mLock.wait(CANCELLATION_WAIT_CHECK_INTERVAL_MS); - } - } catch (InterruptedException e) { - Slog.w(TAG, "Interrupted while waiting for dexopt thread"); - Thread.currentThread().interrupt(); - } - tr.traceEnd(); - } - - private void markDexOptCompleted() { - synchronized (mLock) { - if (mDexOptThread != Thread.currentThread()) { - throw new IllegalStateException( - "Only mDexOptThread can mark completion, mDexOptThread:" + mDexOptThread - + " current:" + Thread.currentThread()); - } - mDexOptThread = null; - // Other threads may be waiting for completion. - mLock.notifyAll(); - } - } - - @GuardedBy("mLock") - private void resetStatesForNewDexOptRunLocked(Thread thread) - throws LegacyDexoptDisabledException { - mDexOptThread = thread; - mLastCancelledPackages.clear(); - controlDexOptBlockingLocked(false); - } - - private void enforceRootOrShell() { - int uid = mInjector.getCallingUid(); - if (uid != Process.ROOT_UID && uid != Process.SHELL_UID) { - throw new SecurityException("Should be shell or root user"); - } - } - - @GuardedBy("mLock") - private void controlDexOptBlockingLocked(boolean block) throws LegacyDexoptDisabledException { - PackageManagerService pm = mInjector.getPackageManagerService(); - mDexOptHelper.controlDexOptBlocking(block); - } - - private void scheduleAJob(int jobId) { - JobScheduler js = mInjector.getJobScheduler(); - JobInfo.Builder builder = - new JobInfo.Builder(jobId, sDexoptServiceName).setRequiresDeviceIdle(true); - if (jobId == JOB_IDLE_OPTIMIZE) { - builder.setRequiresCharging(true).setPeriodic(IDLE_OPTIMIZATION_PERIOD); - } - js.schedule(builder.build()); - } - - private long getLowStorageThreshold() { - long lowThreshold = mInjector.getDataDirStorageLowBytes(); - if (lowThreshold == 0) { - Slog.e(TAG, "Invalid low storage threshold"); - } - - return lowThreshold; - } - - private void logStatus(int status) { - switch (status) { - case STATUS_OK: - Slog.i(TAG, "Idle optimizations completed."); - break; - case STATUS_ABORT_NO_SPACE_LEFT: - Slog.w(TAG, "Idle optimizations aborted because of space constraints."); - break; - case STATUS_ABORT_BY_CANCELLATION: - Slog.w(TAG, "Idle optimizations aborted by cancellation."); - break; - case STATUS_ABORT_THERMAL: - Slog.w(TAG, "Idle optimizations aborted by thermal throttling."); - break; - case STATUS_ABORT_BATTERY: - Slog.w(TAG, "Idle optimizations aborted by low battery."); - break; - case STATUS_DEX_OPT_FAILED: - Slog.w(TAG, "Idle optimizations failed from dexopt."); - break; - default: - Slog.w(TAG, "Idle optimizations ended with unexpected code: " + status); - break; - } - } - - /** - * Returns whether we've successfully run the job. Note that it will return true even if some - * packages may have failed compiling. - */ - private boolean runIdleOptimization(PackageManagerService pm, List<String> pkgs, - boolean isPostBootUpdate) throws LegacyDexoptDisabledException { - synchronized (mLock) { - mLastExecutionStatus = STATUS_UNSPECIFIED; - mLastExecutionStartUptimeMs = SystemClock.uptimeMillis(); - mLastExecutionDurationMs = -1; - } - - int status = STATUS_UNSPECIFIED; - try { - long lowStorageThreshold = getLowStorageThreshold(); - status = idleOptimizePackages(pm, pkgs, lowStorageThreshold, isPostBootUpdate); - logStatus(status); - return status == STATUS_OK || status == STATUS_DEX_OPT_FAILED; - } catch (RuntimeException e) { - status = STATUS_FATAL_ERROR; - throw e; - } finally { - synchronized (mLock) { - mLastExecutionStatus = status; - mLastExecutionDurationMs = SystemClock.uptimeMillis() - mLastExecutionStartUptimeMs; - } - } - } - - /** Gets the size of the directory. It uses recursion to go over all files. */ - private long getDirectorySize(File f) { - long size = 0; - if (f.isDirectory()) { - for (File file : f.listFiles()) { - size += getDirectorySize(file); - } - } else { - size = f.length(); - } - return size; - } - - /** Gets the size of a package. */ - private long getPackageSize(@NonNull Computer snapshot, String pkg) { - // TODO(b/251903639): Make this in line with the calculation in - // `DexOptHelper.DexoptDoneHandler`. - PackageInfo info = snapshot.getPackageInfo(pkg, 0, UserHandle.USER_SYSTEM); - long size = 0; - if (info != null && info.applicationInfo != null) { - File path = Paths.get(info.applicationInfo.sourceDir).toFile(); - if (path.isFile()) { - path = path.getParentFile(); - } - size += getDirectorySize(path); - if (!ArrayUtils.isEmpty(info.applicationInfo.splitSourceDirs)) { - for (String splitSourceDir : info.applicationInfo.splitSourceDirs) { - File pathSplitSourceDir = Paths.get(splitSourceDir).toFile(); - if (pathSplitSourceDir.isFile()) { - pathSplitSourceDir = pathSplitSourceDir.getParentFile(); - } - if (path.getAbsolutePath().equals(pathSplitSourceDir.getAbsolutePath())) { - continue; - } - size += getDirectorySize(pathSplitSourceDir); - } - } - return size; - } - return 0; - } - - @Status - private int idleOptimizePackages(PackageManagerService pm, List<String> pkgs, - long lowStorageThreshold, boolean isPostBootUpdate) - throws LegacyDexoptDisabledException { - ArraySet<String> updatedPackages = new ArraySet<>(); - - try { - boolean supportSecondaryDex = mInjector.supportSecondaryDex(); - - if (supportSecondaryDex) { - @Status int result = reconcileSecondaryDexFiles(); - if (result != STATUS_OK) { - return result; - } - } - - // Only downgrade apps when space is low on device. - // Threshold is selected above the lowStorageThreshold so that we can pro-actively clean - // up disk before user hits the actual lowStorageThreshold. - long lowStorageThresholdForDowngrade = - LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE * lowStorageThreshold; - boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade); - if (DEBUG) { - Slog.d(TAG, "Should Downgrade " + shouldDowngrade); - } - if (shouldDowngrade) { - final Computer snapshot = pm.snapshotComputer(); - Set<String> unusedPackages = - snapshot.getUnusedPackages(mDowngradeUnusedAppsThresholdInMillis); - if (DEBUG) { - Slog.d(TAG, "Unsused Packages " + String.join(",", unusedPackages)); - } - - if (!unusedPackages.isEmpty()) { - for (String pkg : unusedPackages) { - @Status int abortCode = abortIdleOptimizations(/*lowStorageThreshold*/ -1); - if (abortCode != STATUS_OK) { - // Should be aborted by the scheduler. - return abortCode; - } - @DexOptResult - int downgradeResult = downgradePackage(snapshot, pm, pkg, - /* isForPrimaryDex= */ true, isPostBootUpdate); - if (downgradeResult == PackageDexOptimizer.DEX_OPT_PERFORMED) { - updatedPackages.add(pkg); - } - @Status - int status = convertPackageDexOptimizerStatusToInternal(downgradeResult); - if (status != STATUS_OK) { - return status; - } - if (supportSecondaryDex) { - downgradeResult = downgradePackage(snapshot, pm, pkg, - /* isForPrimaryDex= */ false, isPostBootUpdate); - status = convertPackageDexOptimizerStatusToInternal(downgradeResult); - if (status != STATUS_OK) { - return status; - } - } - } - - pkgs = new ArrayList<>(pkgs); - pkgs.removeAll(unusedPackages); - } - } - - return optimizePackages(pkgs, lowStorageThreshold, updatedPackages, isPostBootUpdate); - } finally { - // Always let the pinner service know about changes. - // TODO(b/251903639): ART Service does this for all dexopts, while the code below only - // runs for background jobs. We should try to make them behave the same. - notifyPinService(updatedPackages); - // Only notify IORap the primary dex opt, because we don't want to - // invalidate traces unnecessary due to b/161633001 and that it's - // better to have a trace than no trace at all. - notifyPackagesUpdated(updatedPackages); - } - } - - @Status - private int optimizePackages(List<String> pkgs, long lowStorageThreshold, - ArraySet<String> updatedPackages, boolean isPostBootUpdate) - throws LegacyDexoptDisabledException { - boolean supportSecondaryDex = mInjector.supportSecondaryDex(); - - // Keep the error if there is any error from any package. - @Status int status = STATUS_OK; - - // Other than cancellation, all packages will be processed even if an error happens - // in a package. - for (String pkg : pkgs) { - int abortCode = abortIdleOptimizations(lowStorageThreshold); - if (abortCode != STATUS_OK) { - // Either aborted by the scheduler or no space left. - return abortCode; - } - - @DexOptResult - int primaryResult = optimizePackage(pkg, true /* isForPrimaryDex */, isPostBootUpdate); - if (primaryResult == PackageDexOptimizer.DEX_OPT_CANCELLED) { - return STATUS_ABORT_BY_CANCELLATION; - } - if (primaryResult == PackageDexOptimizer.DEX_OPT_PERFORMED) { - updatedPackages.add(pkg); - } else if (primaryResult == PackageDexOptimizer.DEX_OPT_FAILED) { - status = convertPackageDexOptimizerStatusToInternal(primaryResult); - } - - if (!supportSecondaryDex) { - continue; - } - - @DexOptResult - int secondaryResult = - optimizePackage(pkg, false /* isForPrimaryDex */, isPostBootUpdate); - if (secondaryResult == PackageDexOptimizer.DEX_OPT_CANCELLED) { - return STATUS_ABORT_BY_CANCELLATION; - } - if (secondaryResult == PackageDexOptimizer.DEX_OPT_FAILED) { - status = convertPackageDexOptimizerStatusToInternal(secondaryResult); - } - } - return status; - } - - /** - * Try to downgrade the package to a smaller compilation filter. - * eg. if the package is in speed-profile the package will be downgraded to verify. - * @param pm PackageManagerService - * @param pkg The package to be downgraded. - * @param isForPrimaryDex Apps can have several dex file, primary and secondary. - * @return PackageDexOptimizer.DEX_* - */ - @DexOptResult - private int downgradePackage(@NonNull Computer snapshot, PackageManagerService pm, String pkg, - boolean isForPrimaryDex, boolean isPostBootUpdate) - throws LegacyDexoptDisabledException { - if (DEBUG) { - Slog.d(TAG, "Downgrading " + pkg); - } - if (isCancelling()) { - return PackageDexOptimizer.DEX_OPT_CANCELLED; - } - int reason = PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE; - String filter = getCompilerFilterForReason(reason); - int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE | DexoptOptions.DEXOPT_DOWNGRADE; - - if (isProfileGuidedCompilerFilter(filter)) { - // We don't expect updates in current profiles to be significant here, but - // DEXOPT_CHECK_FOR_PROFILES_UPDATES is set to replicate behaviour that will be - // unconditionally enabled for profile guided filters when ART Service is called instead - // of the legacy PackageDexOptimizer implementation. - dexoptFlags |= DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES; - } - - if (!isPostBootUpdate) { - dexoptFlags |= DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB; - } - - long package_size_before = getPackageSize(snapshot, pkg); - int result = PackageDexOptimizer.DEX_OPT_SKIPPED; - if (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg)) { - // This applies for system apps or if packages location is not a directory, i.e. - // monolithic install. - if (!pm.canHaveOatDir(snapshot, pkg)) { - // For apps that don't have the oat directory, instead of downgrading, - // remove their compiler artifacts from dalvik cache. - pm.deleteOatArtifactsOfPackage(snapshot, pkg); - } else { - result = performDexOptPrimary(pkg, reason, filter, dexoptFlags); - } - } else { - result = performDexOptSecondary(pkg, reason, filter, dexoptFlags); - } - - if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) { - final Computer newSnapshot = pm.snapshotComputer(); - FrameworkStatsLog.write(FrameworkStatsLog.APP_DOWNGRADED, pkg, package_size_before, - getPackageSize(newSnapshot, pkg), /*aggressive=*/false); - } - return result; - } - - @Status - private int reconcileSecondaryDexFiles() throws LegacyDexoptDisabledException { - // TODO(calin): should we denylist packages for which we fail to reconcile? - for (String p : mInjector.getDexManager().getAllPackagesWithSecondaryDexFiles()) { - if (isCancelling()) { - return STATUS_ABORT_BY_CANCELLATION; - } - mInjector.getDexManager().reconcileSecondaryDexFiles(p); - } - return STATUS_OK; - } - - /** - * - * Optimize package if needed. Note that there can be no race between - * concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized. - * @param pkg The package to be downgraded. - * @param isForPrimaryDex Apps can have several dex file, primary and secondary. - * @param isPostBootUpdate is post boot update or not. - * @return PackageDexOptimizer#DEX_OPT_* - */ - @DexOptResult - private int optimizePackage(String pkg, boolean isForPrimaryDex, boolean isPostBootUpdate) - throws LegacyDexoptDisabledException { - int reason = isPostBootUpdate ? PackageManagerService.REASON_POST_BOOT - : PackageManagerService.REASON_BACKGROUND_DEXOPT; - String filter = getCompilerFilterForReason(reason); - - int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE; - if (!isPostBootUpdate) { - dexoptFlags |= DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES - | DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB; - } - - if (isProfileGuidedCompilerFilter(filter)) { - // Ensure DEXOPT_CHECK_FOR_PROFILES_UPDATES is enabled if the filter is profile guided, - // to replicate behaviour that will be unconditionally enabled when ART Service is - // called instead of the legacy PackageDexOptimizer implementation. - dexoptFlags |= DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES; - } - - // System server share the same code path as primary dex files. - // PackageManagerService will select the right optimization path for it. - if (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg)) { - return performDexOptPrimary(pkg, reason, filter, dexoptFlags); - } else { - return performDexOptSecondary(pkg, reason, filter, dexoptFlags); - } - } - - @DexOptResult - private int performDexOptPrimary(String pkg, int reason, String filter, int dexoptFlags) - throws LegacyDexoptDisabledException { - DexoptOptions dexoptOptions = - new DexoptOptions(pkg, reason, filter, /*splitName=*/null, dexoptFlags); - return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/true, - () -> mDexOptHelper.performDexOptWithStatus(dexoptOptions)); - } - - @DexOptResult - private int performDexOptSecondary(String pkg, int reason, String filter, int dexoptFlags) - throws LegacyDexoptDisabledException { - DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason, filter, /*splitName=*/null, - dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX); - return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/false, - () - -> mDexOptHelper.performDexOpt(dexoptOptions) - ? PackageDexOptimizer.DEX_OPT_PERFORMED - : PackageDexOptimizer.DEX_OPT_FAILED); - } - - /** - * Execute the dexopt wrapper and make sure that if performDexOpt wrapper fails - * the package is added to the list of failed packages. - * Return one of following result: - * {@link PackageDexOptimizer#DEX_OPT_SKIPPED} - * {@link PackageDexOptimizer#DEX_OPT_CANCELLED} - * {@link PackageDexOptimizer#DEX_OPT_PERFORMED} - * {@link PackageDexOptimizer#DEX_OPT_FAILED} - */ - @DexOptResult - private int trackPerformDexOpt(String pkg, boolean isForPrimaryDex, - ThrowingCheckedSupplier<Integer, LegacyDexoptDisabledException> performDexOptWrapper) - throws LegacyDexoptDisabledException { - ArraySet<String> failedPackageNames; - synchronized (mLock) { - failedPackageNames = - isForPrimaryDex ? mFailedPackageNamesPrimary : mFailedPackageNamesSecondary; - if (failedPackageNames.contains(pkg)) { - // Skip previously failing package - return PackageDexOptimizer.DEX_OPT_SKIPPED; - } - } - int result = performDexOptWrapper.get(); - if (result == PackageDexOptimizer.DEX_OPT_FAILED) { - synchronized (mLock) { - failedPackageNames.add(pkg); - } - } else if (result == PackageDexOptimizer.DEX_OPT_CANCELLED) { - synchronized (mLock) { - mLastCancelledPackages.add(pkg); - } - } - return result; - } - - @Status - private int convertPackageDexOptimizerStatusToInternal(@DexOptResult int pdoStatus) { - switch (pdoStatus) { - case PackageDexOptimizer.DEX_OPT_CANCELLED: - return STATUS_ABORT_BY_CANCELLATION; - case PackageDexOptimizer.DEX_OPT_FAILED: - return STATUS_DEX_OPT_FAILED; - case PackageDexOptimizer.DEX_OPT_PERFORMED: - case PackageDexOptimizer.DEX_OPT_SKIPPED: - return STATUS_OK; - default: - Slog.e(TAG, "Unkknown error code from PackageDexOptimizer:" + pdoStatus, - new RuntimeException()); - return STATUS_DEX_OPT_FAILED; - } - } - - /** Evaluate whether or not idle optimizations should continue. */ - @Status - private int abortIdleOptimizations(long lowStorageThreshold) { - if (isCancelling()) { - // JobScheduler requested an early abort. - return STATUS_ABORT_BY_CANCELLATION; - } - - // Abort background dexopt if the device is in a moderate or stronger thermal throttling - // state. - int thermalStatus = mInjector.getCurrentThermalStatus(); - if (DEBUG) { - Log.d(TAG, "Thermal throttling status during bgdexopt: " + thermalStatus); - } - if (thermalStatus >= mThermalStatusCutoff) { - return STATUS_ABORT_THERMAL; - } - - if (mInjector.isBatteryLevelLow()) { - return STATUS_ABORT_BATTERY; - } - - long usableSpace = mInjector.getDataDirUsableSpace(); - if (usableSpace < lowStorageThreshold) { - // Rather bail than completely fill up the disk. - Slog.w(TAG, "Aborting background dex opt job due to low storage: " + usableSpace); - return STATUS_ABORT_NO_SPACE_LEFT; - } - - return STATUS_OK; - } - - // Evaluate whether apps should be downgraded. - private boolean shouldDowngrade(long lowStorageThresholdForDowngrade) { - if (mInjector.getDataDirUsableSpace() < lowStorageThresholdForDowngrade) { - return true; - } - - return false; - } - - private boolean isCancelling() { - synchronized (mLock) { - return mDexOptCancellingThread != null; - } - } - - private void markPostBootUpdateCompleted(JobParameters params) { - if (params.getJobId() != JOB_POST_BOOT_UPDATE) { - return; - } - synchronized (mLock) { - if (!mFinishedPostBootUpdate) { - mFinishedPostBootUpdate = true; - } - } - // Safe to do this outside lock. - mInjector.getJobScheduler().cancel(JOB_POST_BOOT_UPDATE); - } - - private void notifyPinService(ArraySet<String> updatedPackages) { - PinnerService pinnerService = mInjector.getPinnerService(); - if (pinnerService != null) { - Slog.i(TAG, "Pinning optimized code " + updatedPackages); - pinnerService.update(updatedPackages, false /* force */); - } - } - - /** Notify all listeners (#addPackagesUpdatedListener) that packages have been updated. */ - private void notifyPackagesUpdated(ArraySet<String> updatedPackages) { - synchronized (mLock) { - for (PackagesUpdatedListener listener : mPackagesUpdatedListeners) { - listener.onPackagesUpdated(updatedPackages); - } - } - } - - private void writeStatsLog(JobParameters params) { - @Status int status; - long durationMs; - long durationIncludingSleepMs; - synchronized (mLock) { - status = mLastExecutionStatus; - durationMs = mLastExecutionDurationMs; - } - - mStatsLogger.write(status, params.getStopReason(), durationMs); - } - - /** Injector pattern for testing purpose */ - @VisibleForTesting - static final class Injector { - private final Context mContext; - private final DexManager mDexManager; - private final PackageManagerService mPackageManagerService; - private final File mDataDir = Environment.getDataDirectory(); - - Injector(Context context, DexManager dexManager, PackageManagerService pm) { - mContext = context; - mDexManager = dexManager; - mPackageManagerService = pm; - } - - int getCallingUid() { - return Binder.getCallingUid(); - } - - Context getContext() { - return mContext; - } - - PackageManagerService getPackageManagerService() { - return mPackageManagerService; - } - - DexOptHelper getDexOptHelper() { - return new DexOptHelper(getPackageManagerService()); - } - - JobScheduler getJobScheduler() { - return mContext.getSystemService(JobScheduler.class); - } - - DexManager getDexManager() { - return mDexManager; - } - - PinnerService getPinnerService() { - return LocalServices.getService(PinnerService.class); - } - - boolean isBackgroundDexOptDisabled() { - return SystemProperties.getBoolean( - "pm.dexopt.disable_bg_dexopt" /* key */, false /* default */); - } - - boolean isBatteryLevelLow() { - return LocalServices.getService(BatteryManagerInternal.class).getBatteryLevelLow(); - } - - long getDowngradeUnusedAppsThresholdInMillis() { - String sysPropKey = "pm.dexopt.downgrade_after_inactive_days"; - String sysPropValue = SystemProperties.get(sysPropKey); - if (sysPropValue == null || sysPropValue.isEmpty()) { - Slog.w(TAG, "SysProp " + sysPropKey + " not set"); - return Long.MAX_VALUE; - } - return TimeUnit.DAYS.toMillis(Long.parseLong(sysPropValue)); - } - - boolean supportSecondaryDex() { - return (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)); - } - - long getDataDirUsableSpace() { - return mDataDir.getUsableSpace(); - } - - long getDataDirStorageLowBytes() { - return mContext.getSystemService(StorageManager.class).getStorageLowBytes(mDataDir); - } - - int getCurrentThermalStatus() { - IThermalService thermalService = IThermalService.Stub.asInterface( - ServiceManager.getService(Context.THERMAL_SERVICE)); - try { - return thermalService.getCurrentThermalStatus(); - } catch (RemoteException e) { - return STATUS_ABORT_THERMAL; - } - } - - int getDexOptThermalCutoff() { - return SystemProperties.getInt( - "dalvik.vm.dexopt.thermal-cutoff", THERMAL_CUTOFF_DEFAULT); - } - - Thread createAndStartThread(String name, Runnable target) { - Thread thread = new Thread(target, name); - Slog.i(TAG, "Starting thread:" + name); - thread.start(); - return thread; - } - } -} diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index b5476fdd3050..2005b17e82a6 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -137,7 +137,7 @@ import com.android.internal.util.CollectionUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; import com.android.modules.utils.TypedXmlSerializer; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; +import com.android.server.ondeviceintelligence.OnDeviceIntelligenceManagerInternal; import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.PackageDexUsage; import com.android.server.pm.parsing.PackageInfoUtils; @@ -419,7 +419,6 @@ public class ComputerEngine implements Computer { private final PackageDexOptimizer mPackageDexOptimizer; private final DexManager mDexManager; private final CompilerStats mCompilerStats; - private final BackgroundDexOptService mBackgroundDexOptService; private final PackageManagerInternal.ExternalSourcesPolicy mExternalSourcesPolicy; private final CrossProfileIntentResolverEngine mCrossProfileIntentResolverEngine; @@ -472,7 +471,6 @@ public class ComputerEngine implements Computer { mPackageDexOptimizer = args.service.mPackageDexOptimizer; mDexManager = args.service.getDexManager(); mCompilerStats = args.service.mCompilerStats; - mBackgroundDexOptService = args.service.mBackgroundDexOptService; mExternalSourcesPolicy = args.service.mExternalSourcesPolicy; mCrossProfileIntentResolverEngine = new CrossProfileIntentResolverEngine( mUserManager, mDomainVerificationManager, mDefaultAppProvider, mContext); @@ -3093,40 +3091,7 @@ public class ComputerEngine implements Computer { } ipw.println("Dexopt state:"); ipw.increaseIndent(); - if (DexOptHelper.useArtService()) { - DexOptHelper.dumpDexoptState(ipw, packageName); - } else { - Collection<? extends PackageStateInternal> pkgSettings; - if (setting != null) { - pkgSettings = Collections.singletonList(setting); - } else { - pkgSettings = mSettings.getPackages().values(); - } - - for (PackageStateInternal pkgSetting : pkgSettings) { - final AndroidPackage pkg = pkgSetting.getPkg(); - if (pkg == null || pkg.isApex()) { - // Skip APEX which is not dex-optimized - continue; - } - final String pkgName = pkg.getPackageName(); - ipw.println("[" + pkgName + "]"); - ipw.increaseIndent(); - - // TODO(b/251903639): Call into ART Service. - try { - mPackageDexOptimizer.dumpDexoptState(ipw, pkg, pkgSetting, - mDexManager.getPackageUseInfoOrDefault(pkgName)); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - ipw.decreaseIndent(); - } - ipw.println("BgDexopt state:"); - ipw.increaseIndent(); - mBackgroundDexOptService.dump(ipw); - ipw.decreaseIndent(); - } + DexOptHelper.dumpDexoptState(ipw, packageName); ipw.decreaseIndent(); break; } @@ -4389,9 +4354,8 @@ public class ComputerEngine implements Computer { if (Process.isSdkSandboxUid(uid)) { uid = getBaseSdkSandboxUid(); } - if (Process.isIsolatedUid(uid) - && mPermissionManager.getHotwordDetectionServiceProvider() != null - && uid == mPermissionManager.getHotwordDetectionServiceProvider().getUid()) { + final int callingUserId = UserHandle.getUserId(callingUid); + if (isKnownIsolatedComputeApp(uid, callingUserId)) { try { uid = getIsolatedOwner(uid); } catch (IllegalStateException e) { @@ -4399,7 +4363,6 @@ public class ComputerEngine implements Computer { Slog.wtf(TAG, "Expected isolated uid " + uid + " to have an owner", e); } } - final int callingUserId = UserHandle.getUserId(callingUid); final int appId = UserHandle.getAppId(uid); final Object obj = mSettings.getSettingBase(appId); if (obj instanceof SharedUserSetting) { @@ -4435,9 +4398,7 @@ public class ComputerEngine implements Computer { if (Process.isSdkSandboxUid(uid)) { uid = getBaseSdkSandboxUid(); } - if (Process.isIsolatedUid(uid) - && mPermissionManager.getHotwordDetectionServiceProvider() != null - && uid == mPermissionManager.getHotwordDetectionServiceProvider().getUid()) { + if (isKnownIsolatedComputeApp(uid, callingUserId)) { try { uid = getIsolatedOwner(uid); } catch (IllegalStateException e) { @@ -5838,6 +5799,43 @@ public class ComputerEngine implements Computer { return getPackage(mService.getSdkSandboxPackageName()).getUid(); } + + private boolean isKnownIsolatedComputeApp(int uid, int callingUserId) { + if (!Process.isIsolatedUid(uid)) { + return false; + } + final boolean isHotword = + mPermissionManager.getHotwordDetectionServiceProvider() != null + && uid + == mPermissionManager.getHotwordDetectionServiceProvider().getUid(); + if (isHotword) { + return true; + } + OnDeviceIntelligenceManagerInternal onDeviceIntelligenceManagerInternal = + mInjector.getLocalService(OnDeviceIntelligenceManagerInternal.class); + if (onDeviceIntelligenceManagerInternal == null) { + return false; + } + + String onDeviceIntelligencePackage = + onDeviceIntelligenceManagerInternal.getRemoteServicePackageName(); + if (onDeviceIntelligencePackage == null) { + return false; + } + + try { + if (getIsolatedOwner(uid) == getPackageUid(onDeviceIntelligencePackage, 0, + callingUserId)) { + return true; + } + } catch (IllegalStateException e) { + // If the owner uid doesn't exist, just use the current uid + Slog.wtf(TAG, "Expected isolated uid " + uid + " to have an owner", e); + } + + return false; + } + @Nullable @Override public SharedUserApi getSharedUser(int sharedUserAppId) { diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java index 588c6291f2f1..fd162214031c 100644 --- a/services/core/java/com/android/server/pm/DeletePackageHelper.java +++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java @@ -542,7 +542,8 @@ final class DeletePackageHelper { final Computer snapshot = mPm.snapshotComputer(); for (final int affectedUserId : outInfo.mRemovedUsers) { if (hadSuspendAppsPermission.get(affectedUserId)) { - mPm.unsuspendForSuspendingPackage(snapshot, packageName, affectedUserId); + mPm.unsuspendForSuspendingPackage(snapshot, packageName, + affectedUserId /*suspendingUserId*/, true /*inAllUsers*/); mPm.removeAllDistractingPackageRestrictions(snapshot, affectedUserId); } } diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index ecfc768a874e..c60f0afcc2ff 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -23,7 +23,6 @@ import static android.os.incremental.IncrementalManager.isIncrementalPath; import static com.android.server.LocalManagerRegistry.ManagerNotFoundException; import static com.android.server.pm.ApexManager.ActiveApexInfo; -import static com.android.server.pm.InstructionSets.getAppDexInstructionSets; import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import static com.android.server.pm.PackageManagerService.REASON_BOOT_AFTER_MAINLINE_UPDATE; @@ -32,10 +31,7 @@ import static com.android.server.pm.PackageManagerService.REASON_CMDLINE; import static com.android.server.pm.PackageManagerService.REASON_FIRST_BOOT; import static com.android.server.pm.PackageManagerService.SCAN_AS_APEX; import static com.android.server.pm.PackageManagerService.SCAN_AS_INSTANT_APP; -import static com.android.server.pm.PackageManagerService.STUB_SUFFIX; import static com.android.server.pm.PackageManagerService.TAG; -import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason; -import static com.android.server.pm.PackageManagerServiceCompilerMapping.getDefaultCompilerFilter; import static com.android.server.pm.PackageManagerServiceUtils.REMOVE_IF_APEX_PKG; import static com.android.server.pm.PackageManagerServiceUtils.REMOVE_IF_NULL_PKG; import static com.android.server.pm.PackageManagerServiceUtils.getPackageManagerLocal; @@ -45,19 +41,15 @@ import static dalvik.system.DexFile.isProfileGuidedCompilerFilter; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppGlobals; -import android.app.role.RoleManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApexStagedEvent; -import android.content.pm.Flags; import android.content.pm.IPackageManagerNative; import android.content.pm.IStagedApexObserver; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.content.pm.SharedLibraryInfo; -import android.content.pm.dex.ArtManager; import android.os.Binder; import android.os.RemoteException; import android.os.ServiceManager; @@ -83,8 +75,6 @@ import com.android.server.art.ReasonMapping; import com.android.server.art.model.ArtFlags; import com.android.server.art.model.DexoptParams; import com.android.server.art.model.DexoptResult; -import com.android.server.pm.Installer.InstallerException; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.PackageDexOptimizer.DexOptResult; import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.DexoptOptions; @@ -131,228 +121,6 @@ public final class DexOptHelper { } /** - * Performs dexopt on the set of packages in {@code packages} and returns an int array - * containing statistics about the invocation. The array consists of three elements, - * which are (in order) {@code numberOfPackagesOptimized}, {@code numberOfPackagesSkipped} - * and {@code numberOfPackagesFailed}. - */ - public int[] performDexOptUpgrade(List<PackageStateInternal> packageStates, - final int compilationReason, boolean bootComplete) - throws LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); - int numberOfPackagesVisited = 0; - int numberOfPackagesOptimized = 0; - int numberOfPackagesSkipped = 0; - int numberOfPackagesFailed = 0; - final int numberOfPackagesToDexopt = packageStates.size(); - - for (var packageState : packageStates) { - var pkg = packageState.getAndroidPackage(); - numberOfPackagesVisited++; - - boolean useProfileForDexopt = false; - - if ((mPm.isFirstBoot() || mPm.isDeviceUpgrading()) && packageState.isSystem()) { - // Copy over initial preopt profiles since we won't get any JIT samples for methods - // that are already compiled. - File profileFile = new File(getPrebuildProfilePath(pkg)); - // Copy profile if it exists. - if (profileFile.exists()) { - try { - // We could also do this lazily before calling dexopt in - // PackageDexOptimizer to prevent this happening on first boot. The issue - // is that we don't have a good way to say "do this only once". - if (!mPm.mInstaller.copySystemProfile(profileFile.getAbsolutePath(), - pkg.getUid(), pkg.getPackageName(), - ArtManager.getProfileName(null))) { - Log.e(TAG, "Installer failed to copy system profile!"); - } else { - // Disabled as this causes speed-profile compilation during first boot - // even if things are already compiled. - // useProfileForDexopt = true; - } - } catch (InstallerException | RuntimeException e) { - Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath() + " ", - e); - } - } else { - PackageSetting disabledPs = mPm.mSettings.getDisabledSystemPkgLPr( - pkg.getPackageName()); - // Handle compressed APKs in this path. Only do this for stubs with profiles to - // minimize the number off apps being speed-profile compiled during first boot. - // The other paths will not change the filter. - if (disabledPs != null && disabledPs.getPkg().isStub()) { - // The package is the stub one, remove the stub suffix to get the normal - // package and APK names. - String systemProfilePath = getPrebuildProfilePath(disabledPs.getPkg()) - .replace(STUB_SUFFIX, ""); - profileFile = new File(systemProfilePath); - // If we have a profile for a compressed APK, copy it to the reference - // location. - // Note that copying the profile here will cause it to override the - // reference profile every OTA even though the existing reference profile - // may have more data. We can't copy during decompression since the - // directories are not set up at that point. - if (profileFile.exists()) { - try { - // We could also do this lazily before calling dexopt in - // PackageDexOptimizer to prevent this happening on first boot. The - // issue is that we don't have a good way to say "do this only - // once". - if (!mPm.mInstaller.copySystemProfile(profileFile.getAbsolutePath(), - pkg.getUid(), pkg.getPackageName(), - ArtManager.getProfileName(null))) { - Log.e(TAG, "Failed to copy system profile for stub package!"); - } else { - useProfileForDexopt = true; - } - } catch (InstallerException | RuntimeException e) { - Log.e(TAG, "Failed to copy profile " - + profileFile.getAbsolutePath() + " ", e); - } - } - } - } - } - - if (!mPm.mPackageDexOptimizer.canOptimizePackage(pkg)) { - if (DEBUG_DEXOPT) { - Log.i(TAG, "Skipping update of non-optimizable app " + pkg.getPackageName()); - } - numberOfPackagesSkipped++; - continue; - } - - if (DEBUG_DEXOPT) { - Log.i(TAG, "Updating app " + numberOfPackagesVisited + " of " - + numberOfPackagesToDexopt + ": " + pkg.getPackageName()); - } - - int pkgCompilationReason = compilationReason; - if (useProfileForDexopt) { - // Use background dexopt mode to try and use the profile. Note that this does not - // guarantee usage of the profile. - pkgCompilationReason = PackageManagerService.REASON_BACKGROUND_DEXOPT; - } - - int dexoptFlags = bootComplete ? DexoptOptions.DEXOPT_BOOT_COMPLETE : 0; - - String filter = getCompilerFilterForReason(pkgCompilationReason); - if (isProfileGuidedCompilerFilter(filter)) { - // DEXOPT_CHECK_FOR_PROFILES_UPDATES used to be false to avoid merging profiles - // during boot which might interfere with background compilation (b/28612421). - // However those problems were related to the verify-profile compiler filter which - // doesn't exist any more, so enable it again. - dexoptFlags |= DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES; - } - - if (compilationReason == REASON_FIRST_BOOT) { - // TODO: This doesn't cover the upgrade case, we should check for this too. - dexoptFlags |= DexoptOptions.DEXOPT_INSTALL_WITH_DEX_METADATA_FILE; - } - int primaryDexOptStatus = performDexOptTraced( - new DexoptOptions(pkg.getPackageName(), pkgCompilationReason, filter, - /*splitName*/ null, dexoptFlags)); - - switch (primaryDexOptStatus) { - case PackageDexOptimizer.DEX_OPT_PERFORMED: - numberOfPackagesOptimized++; - break; - case PackageDexOptimizer.DEX_OPT_SKIPPED: - numberOfPackagesSkipped++; - break; - case PackageDexOptimizer.DEX_OPT_CANCELLED: - // ignore this case - break; - case PackageDexOptimizer.DEX_OPT_FAILED: - numberOfPackagesFailed++; - break; - default: - Log.e(TAG, "Unexpected dexopt return code " + primaryDexOptStatus); - break; - } - } - - return new int[]{numberOfPackagesOptimized, numberOfPackagesSkipped, - numberOfPackagesFailed}; - } - - /** - * Checks if system UI package (typically "com.android.systemui") needs to be re-compiled, and - * compiles it if needed. - */ - private void checkAndDexOptSystemUi(int reason) throws LegacyDexoptDisabledException { - Computer snapshot = mPm.snapshotComputer(); - String sysUiPackageName = - mPm.mContext.getString(com.android.internal.R.string.config_systemUi); - AndroidPackage pkg = snapshot.getPackage(sysUiPackageName); - if (pkg == null) { - Log.w(TAG, "System UI package " + sysUiPackageName + " is not found for dexopting"); - return; - } - - String defaultCompilerFilter = getCompilerFilterForReason(reason); - String targetCompilerFilter = - SystemProperties.get("dalvik.vm.systemuicompilerfilter", defaultCompilerFilter); - String compilerFilter; - - if (isProfileGuidedCompilerFilter(targetCompilerFilter)) { - compilerFilter = "verify"; - File profileFile = new File(getPrebuildProfilePath(pkg)); - - // Copy the profile to the reference profile path if it exists. Installd can only use a - // profile at the reference profile path for dexopt. - if (profileFile.exists()) { - try { - synchronized (mPm.mInstallLock) { - if (mPm.mInstaller.copySystemProfile(profileFile.getAbsolutePath(), - pkg.getUid(), pkg.getPackageName(), - ArtManager.getProfileName(null))) { - compilerFilter = targetCompilerFilter; - } else { - Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath()); - } - } - } catch (InstallerException | RuntimeException e) { - Log.e(TAG, "Failed to copy profile " + profileFile.getAbsolutePath(), e); - } - } - } else { - compilerFilter = targetCompilerFilter; - } - - performDexoptPackage(sysUiPackageName, reason, compilerFilter); - } - - private void dexoptLauncher(int reason) throws LegacyDexoptDisabledException { - Computer snapshot = mPm.snapshotComputer(); - RoleManager roleManager = mPm.mContext.getSystemService(RoleManager.class); - for (var packageName : roleManager.getRoleHolders(RoleManager.ROLE_HOME)) { - AndroidPackage pkg = snapshot.getPackage(packageName); - if (pkg == null) { - Log.w(TAG, "Launcher package " + packageName + " is not found for dexopting"); - } else { - performDexoptPackage(packageName, reason, "speed-profile"); - } - } - } - - private void performDexoptPackage(@NonNull String packageName, int reason, - @NonNull String compilerFilter) throws LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); - - // DEXOPT_CHECK_FOR_PROFILES_UPDATES is set to replicate behaviour that will be - // unconditionally enabled for profile guided filters when ART Service is called instead of - // the legacy PackageDexOptimizer implementation. - int dexoptFlags = isProfileGuidedCompilerFilter(compilerFilter) - ? DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES - : 0; - - performDexOptTraced(new DexoptOptions( - packageName, reason, compilerFilter, null /* splitName */, dexoptFlags)); - } - - /** * Called during startup to do any boot time dexopting. This can occasionally be time consuming * (30+ seconds) and the function will block until it is complete. */ @@ -377,35 +145,9 @@ public final class DexOptHelper { final long startTime = System.nanoTime(); - if (useArtService()) { - mBootDexoptStartTime = startTime; - getArtManagerLocal().onBoot(DexoptOptions.convertToArtServiceDexoptReason(reason), - null /* progressCallbackExecutor */, null /* progressCallback */); - } else { - try { - // System UI and the launcher are important to user experience, so we check them - // after a mainline update or OTA. They may need to be re-compiled in these cases. - checkAndDexOptSystemUi(reason); - dexoptLauncher(reason); - - if (reason != REASON_BOOT_AFTER_OTA && reason != REASON_FIRST_BOOT) { - return; - } - - final Computer snapshot = mPm.snapshotComputer(); - - // TODO(b/251903639): Align this with how ART Service selects packages for boot - // compilation. - List<PackageStateInternal> pkgSettings = - getPackagesForDexopt(snapshot.getPackageStates().values(), mPm); - - final int[] stats = - performDexOptUpgrade(pkgSettings, reason, false /* bootComplete */); - reportBootDexopt(startTime, stats[0], stats[1], stats[2]); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - } + mBootDexoptStartTime = startTime; + getArtManagerLocal().onBoot(DexoptOptions.convertToArtServiceDexoptReason(reason), + null /* progressCallbackExecutor */, null /* progressCallback */); } private void reportBootDexopt(long startTime, int numDexopted, int numSkipped, int numFailed) { @@ -450,15 +192,7 @@ public final class DexOptHelper { @DexOptResult int dexoptStatus; if (options.isDexoptOnlySecondaryDex()) { - if (useArtService()) { - dexoptStatus = performDexOptWithArtService(options, 0 /* extraFlags */); - } else { - try { - return mPm.getDexManager().dexoptSecondaryDex(options); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - } + dexoptStatus = performDexOptWithArtService(options, 0 /* extraFlags */); } else { dexoptStatus = performDexOptWithStatus(options); } @@ -491,39 +225,11 @@ public final class DexOptHelper { // if the package can now be considered up to date for the given filter. @DexOptResult private int performDexOptInternal(DexoptOptions options) { - if (useArtService()) { - return performDexOptWithArtService(options, ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES); - } - - AndroidPackage p; - PackageSetting pkgSetting; - synchronized (mPm.mLock) { - p = mPm.mPackages.get(options.getPackageName()); - pkgSetting = mPm.mSettings.getPackageLPr(options.getPackageName()); - if (p == null || pkgSetting == null) { - // Package could not be found. Report failure. - return PackageDexOptimizer.DEX_OPT_FAILED; - } - if (p.isApex()) { - // APEX needs no dexopt - return PackageDexOptimizer.DEX_OPT_SKIPPED; - } - mPm.getPackageUsage().maybeWriteAsync(mPm.mSettings.getPackagesLocked()); - mPm.mCompilerStats.maybeWriteAsync(); - } - final long callingId = Binder.clearCallingIdentity(); - try { - return performDexOptInternalWithDependenciesLI(p, pkgSetting, options); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } finally { - Binder.restoreCallingIdentity(callingId); - } + return performDexOptWithArtService(options, ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES); } /** - * Performs dexopt on the given package using ART Service. May only be called when ART Service - * is enabled, i.e. when {@link useArtService} returns true. + * Performs dexopt on the given package using ART Service. */ @DexOptResult private int performDexOptWithArtService(DexoptOptions options, @@ -545,91 +251,6 @@ public final class DexOptHelper { } } - @DexOptResult - private int performDexOptInternalWithDependenciesLI( - AndroidPackage p, @NonNull PackageStateInternal pkgSetting, DexoptOptions options) - throws LegacyDexoptDisabledException { - if (PLATFORM_PACKAGE_NAME.equals(p.getPackageName())) { - // This needs to be done in odrefresh in early boot, for security reasons. - throw new IllegalArgumentException("Cannot dexopt the system server"); - } - - // Select the dex optimizer based on the force parameter. - // Note: The force option is rarely used (cmdline input for testing, mostly), so it's OK to - // allocate an object here. - PackageDexOptimizer pdo = options.isForce() - ? new PackageDexOptimizer.ForcedUpdatePackageDexOptimizer(mPm.mPackageDexOptimizer) - : mPm.mPackageDexOptimizer; - - // Dexopt all dependencies first. Note: we ignore the return value and march on - // on errors. - // Note that we are going to call performDexOpt on those libraries as many times as - // they are referenced in packages. When we do a batch of performDexOpt (for example - // at boot, or background job), the passed 'targetCompilerFilter' stays the same, - // and the first package that uses the library will dexopt it. The - // others will see that the compiled code for the library is up to date. - Collection<SharedLibraryInfo> deps = SharedLibraryUtils.findSharedLibraries(pkgSetting); - final String[] instructionSets = getAppDexInstructionSets( - pkgSetting.getPrimaryCpuAbi(), - pkgSetting.getSecondaryCpuAbi()); - if (!deps.isEmpty()) { - DexoptOptions libraryOptions = new DexoptOptions(options.getPackageName(), - options.getCompilationReason(), options.getCompilerFilter(), - options.getSplitName(), - options.getFlags() | DexoptOptions.DEXOPT_AS_SHARED_LIBRARY); - for (SharedLibraryInfo info : deps) { - Computer snapshot = mPm.snapshotComputer(); - AndroidPackage depPackage = snapshot.getPackage(info.getPackageName()); - PackageStateInternal depPackageStateInternal = - snapshot.getPackageStateInternal(info.getPackageName()); - if (depPackage != null && depPackageStateInternal != null) { - // TODO: Analyze and investigate if we (should) profile libraries. - pdo.performDexOpt(depPackage, depPackageStateInternal, instructionSets, - mPm.getOrCreateCompilerPackageStats(depPackage), - mPm.getDexManager().getPackageUseInfoOrDefault( - depPackage.getPackageName()), libraryOptions); - } else { - // TODO(ngeoffray): Support dexopting system shared libraries. - } - } - } - - return pdo.performDexOpt(p, pkgSetting, instructionSets, - mPm.getOrCreateCompilerPackageStats(p), - mPm.getDexManager().getPackageUseInfoOrDefault(p.getPackageName()), options); - } - - /** @deprecated For legacy shell command only. */ - @Deprecated - public void forceDexOpt(@NonNull Computer snapshot, String packageName) - throws LegacyDexoptDisabledException { - PackageManagerServiceUtils.enforceSystemOrRoot("forceDexOpt"); - - final PackageStateInternal packageState = snapshot.getPackageStateInternal(packageName); - final AndroidPackage pkg = packageState == null ? null : packageState.getPkg(); - if (packageState == null || pkg == null) { - throw new IllegalArgumentException("Unknown package: " + packageName); - } - if (pkg.isApex()) { - throw new IllegalArgumentException("Can't dexopt APEX package: " + packageName); - } - - Trace.traceBegin(TRACE_TAG_DALVIK, "dexopt"); - - // Whoever is calling forceDexOpt wants a compiled package. - // Don't use profiles since that may cause compilation to be skipped. - DexoptOptions options = new DexoptOptions(packageName, REASON_CMDLINE, - getDefaultCompilerFilter(), null /* splitName */, - DexoptOptions.DEXOPT_FORCE | DexoptOptions.DEXOPT_BOOT_COMPLETE); - - @DexOptResult int res = performDexOptInternalWithDependenciesLI(pkg, packageState, options); - - Trace.traceEnd(TRACE_TAG_DALVIK); - if (res != PackageDexOptimizer.DEX_OPT_PERFORMED) { - throw new IllegalStateException("Failed to dexopt: " + res); - } - } - public boolean performDexOptMode(@NonNull Computer snapshot, String packageName, String targetCompilerFilter, boolean force, boolean bootComplete, String splitName) { if (!PackageManagerServiceUtils.isSystemOrRootOrShell() @@ -872,10 +493,6 @@ public final class DexOptHelper { } } - /*package*/ void controlDexOptBlocking(boolean block) throws LegacyDexoptDisabledException { - mPm.mPackageDexOptimizer.controlDexOptBlocking(block); - } - /** * Dumps the dexopt state for the given package, or all packages if it is null. */ @@ -935,19 +552,9 @@ public final class DexOptHelper { } /** - * Returns true if ART Service should be used for package optimization. - */ - public static boolean useArtService() { - return SystemProperties.getBoolean("dalvik.vm.useartservice", false); - } - - /** * Returns {@link DexUseManagerLocal} if ART Service should be used for package optimization. */ public static @Nullable DexUseManagerLocal getDexUseManagerLocal() { - if (!useArtService()) { - return null; - } try { return LocalManagerRegistry.getManagerOrThrow(DexUseManagerLocal.class); } catch (ManagerNotFoundException e) { @@ -1039,10 +646,6 @@ public final class DexOptHelper { */ public static void initializeArtManagerLocal( @NonNull Context systemContext, @NonNull PackageManagerService pm) { - if (!useArtService()) { - return; - } - ArtManagerLocal artManager = new ArtManagerLocal(systemContext); artManager.addDexoptDoneCallback(false /* onlyIncludeUpdates */, Runnable::run, pm.getDexOptHelper().new DexoptDoneHandler()); @@ -1059,9 +662,7 @@ public final class DexOptHelper { } }, new IntentFilter(Intent.ACTION_LOCKED_BOOT_COMPLETED)); - if (Flags.useArtServiceV2()) { - StagedApexObserver.registerForStagedApexUpdates(artManager); - } + StagedApexObserver.registerForStagedApexUpdates(artManager); } /** @@ -1146,9 +747,7 @@ public final class DexOptHelper { & PackageManager.INSTALL_IGNORE_DEXOPT_PROFILE) != 0; /*@DexoptFlags*/ int extraFlags = - ignoreDexoptProfile && Flags.useArtServiceV2() - ? ArtFlags.FLAG_IGNORE_PROFILE - : 0; + ignoreDexoptProfile ? ArtFlags.FLAG_IGNORE_PROFILE : 0; DexoptParams params = dexoptOptions.convertToDexoptParams(extraFlags); DexoptResult dexOptResult = getArtManagerLocal().dexoptPackage( snapshot, packageName, params); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index ae68018c90b3..c559892327df 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -46,10 +46,7 @@ import static android.os.storage.StorageManager.FLAG_STORAGE_DE; import static android.os.storage.StorageManager.FLAG_STORAGE_EXTERNAL; import static com.android.internal.pm.pkg.parsing.ParsingPackageUtils.APP_METADATA_FILE_NAME; -import static com.android.server.pm.DexOptHelper.useArtService; import static com.android.server.pm.InstructionSets.getAppDexInstructionSets; -import static com.android.server.pm.InstructionSets.getDexCodeInstructionSet; -import static com.android.server.pm.InstructionSets.getPreferredInstructionSet; import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION; import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL; import static com.android.server.pm.PackageManagerService.DEBUG_PACKAGE_SCANNING; @@ -173,7 +170,6 @@ import com.android.server.EventLogTags; import com.android.server.SystemConfig; import com.android.server.art.model.DexoptResult; import com.android.server.criticalevents.CriticalEventLog; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.DexoptOptions; @@ -272,8 +268,6 @@ final class InstallPackageHelper { final PackageSetting oldPkgSetting = request.getScanRequestOldPackageSetting(); final PackageSetting originalPkgSetting = request.getScanRequestOriginalPackageSetting(); final String realPkgName = request.getRealPackageName(); - final List<String> changedAbiCodePath = - useArtService() ? null : request.getChangedAbiCodePath(); final PackageSetting pkgSetting; if (request.getScanRequestPackageSetting() != null) { SharedUserSetting requestSharedUserSetting = mPm.mSettings.getSharedUserSettingLPr( @@ -449,23 +443,6 @@ final class InstallPackageHelper { } pkgSetting.setSigningDetails(reconciledPkg.mSigningDetails); - // The conditional on useArtService() for changedAbiCodePath above means this is skipped - // when ART Service is in use, since it has its own dex file GC. - if (changedAbiCodePath != null && changedAbiCodePath.size() > 0) { - for (int i = changedAbiCodePath.size() - 1; i >= 0; --i) { - final String codePathString = changedAbiCodePath.get(i); - try { - synchronized (mPm.mInstallLock) { - mPm.mInstaller.rmdex(codePathString, - getDexCodeInstructionSet(getPreferredInstructionSet())); - } - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } catch (Installer.InstallerException ignored) { - } - } - } - final int userId = request.getUserId(); // Modify state for the given package setting commitPackageSettings(pkg, pkgSetting, oldPkgSetting, reconciledPkg); @@ -2538,20 +2515,6 @@ final class InstallPackageHelper { pkg.getBaseApkPath(), pkg.getSplitCodePaths()); } - // ART Service handles this on demand instead. - if (!useArtService() && pkg != null) { - // Prepare the application profiles for the new code paths. - // This needs to be done before invoking dexopt so that any install-time profile - // can be used for optimizations. - try { - mArtManagerService.prepareAppProfiles(pkg, - mPm.resolveUserIds(installRequest.getUserId()), - /* updateReferenceProfileContent= */ true); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - } - // Construct the DexoptOptions early to see if we should skip running dexopt. // // Do not run PackageDexOptimizer through the local performDexOpt @@ -2602,36 +2565,11 @@ final class InstallPackageHelper { realPkgSetting.getPkgState().setUpdatedSystemApp(isUpdatedSystemApp); - if (useArtService()) { - DexoptResult dexOptResult = DexOptHelper.dexoptPackageUsingArtService( - installRequest, dexoptOptions); - installRequest.onDexoptFinished(dexOptResult); - } else { - try { - mPackageDexOptimizer.performDexOpt(pkg, realPkgSetting, - null /* instructionSets */, - mPm.getOrCreateCompilerPackageStats(pkg), - mDexManager.getPackageUseInfoOrDefault(packageName), dexoptOptions); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - } + DexoptResult dexOptResult = + DexOptHelper.dexoptPackageUsingArtService(installRequest, dexoptOptions); + installRequest.onDexoptFinished(dexOptResult); Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } - - if (!useArtService()) { - // Notify BackgroundDexOptService that the package has been changed. - // If this is an update of a package which used to fail to compile, - // BackgroundDexOptService will remove it from its denylist. - // ART Service currently doesn't support this and will retry packages in every - // background dexopt. - // TODO: Layering violation - try { - BackgroundDexOptService.getService().notifyPackageChanged(packageName); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - } } PackageManagerServiceUtils.waitForNativeBinariesExtractionForIncremental( incrementalStorages); diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java index 43075a232a23..c10196f1ce9b 100644 --- a/services/core/java/com/android/server/pm/InstallRequest.java +++ b/services/core/java/com/android/server/pm/InstallRequest.java @@ -35,7 +35,6 @@ import android.apex.ApexInfo; import android.app.AppOpsManager; import android.content.pm.ArchivedPackageParcel; import android.content.pm.DataLoaderType; -import android.content.pm.Flags; import android.content.pm.IPackageInstallObserver2; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; @@ -951,7 +950,7 @@ final class InstallRequest { // Only report external profile warnings when installing from adb. The goal is to warn app // developers if they have provided bad external profiles, so it's not beneficial to report // those warnings in the normal app install workflow. - if (isInstallFromAdb() && Flags.useArtServiceV2()) { + if (isInstallFromAdb()) { var externalProfileErrors = new LinkedHashSet<String>(); for (PackageDexoptResult packageResult : dexoptResult.getPackageDexoptResults()) { for (DexContainerFileDexoptResult fileResult : diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java index 34903d1ed47d..8038c9a8cb30 100644 --- a/services/core/java/com/android/server/pm/Installer.java +++ b/services/core/java/com/android/server/pm/Installer.java @@ -16,8 +16,6 @@ package com.android.server.pm; -import static com.android.server.pm.DexOptHelper.useArtService; - import android.annotation.AppIdInt; import android.annotation.NonNull; import android.annotation.Nullable; @@ -97,15 +95,6 @@ public class Installer extends SystemService { */ public static final int PROFILE_ANALYSIS_DONT_OPTIMIZE_EMPTY_PROFILES = 3; - /** - * The results of {@code getOdexVisibility}. See - * {@link #getOdexVisibility(String, String, String)} for details. - */ - public static final int ODEX_NOT_FOUND = 0; - public static final int ODEX_IS_PUBLIC = 1; - public static final int ODEX_IS_PRIVATE = 2; - - public static final int FLAG_STORAGE_DE = IInstalld.FLAG_STORAGE_DE; public static final int FLAG_STORAGE_CE = IInstalld.FLAG_STORAGE_CE; public static final int FLAG_STORAGE_EXTERNAL = IInstalld.FLAG_STORAGE_EXTERNAL; @@ -611,37 +600,7 @@ public class Installer extends SystemService { } /** - * Runs dex optimization. - * - * @param apkPath Path of target APK - * @param uid UID of the package - * @param pkgName Name of the package - * @param instructionSet Target instruction set to run dex optimization. - * @param dexoptNeeded Necessary dex optimization for this request. Check - * {@link dalvik.system.DexFile#NO_DEXOPT_NEEDED}, - * {@link dalvik.system.DexFile#DEX2OAT_FROM_SCRATCH}, - * {@link dalvik.system.DexFile#DEX2OAT_FOR_BOOT_IMAGE}, and - * {@link dalvik.system.DexFile#DEX2OAT_FOR_FILTER}. - * @param outputPath Output path of generated dex optimization. - * @param dexFlags Check {@code DEXOPT_*} for allowed flags. - * @param compilerFilter Compiler filter like "verify", "speed-profile". Check - * {@code art/libartbase/base/compiler_filter.cc} for full list. - * @param volumeUuid UUID of the volume where the package data is stored. {@code null} - * represents internal storage. - * @param classLoaderContext This encodes the class loader chain (class loader type + class - * path) in a format compatible to dex2oat. Check - * {@code DexoptUtils.processContextForDexLoad} for further details. - * @param seInfo Selinux context to set for generated outputs. - * @param downgrade If set, allows downgrading {@code compilerFilter}. If downgrading is not - * allowed and requested {@code compilerFilter} is considered as downgrade, - * the request will be ignored. - * @param targetSdkVersion Target SDK version of the package. - * @param profileName Name of reference profile file. - * @param dexMetadataPath Specifies the location of dex metadata file. - * @param compilationReason Specifies the reason for the compilation like "install". - * @return {@code true} if {@code dexopt} is completed. {@code false} if it was cancelled. - * - * @throws InstallerException if {@code dexopt} fails. + * This function only remains to allow overriding in OtaDexoptService. */ public boolean dexopt(String apkPath, int uid, String pkgName, String instructionSet, int dexoptNeeded, @Nullable String outputPath, int dexFlags, String compilerFilter, @@ -650,98 +609,7 @@ public class Installer extends SystemService { @Nullable String profileName, @Nullable String dexMetadataPath, @Nullable String compilationReason) throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - assertValidInstructionSet(instructionSet); - BlockGuard.getVmPolicy().onPathAccess(apkPath); - BlockGuard.getVmPolicy().onPathAccess(outputPath); - BlockGuard.getVmPolicy().onPathAccess(dexMetadataPath); - if (!checkBeforeRemote()) return false; - try { - return mInstalld.dexopt(apkPath, uid, pkgName, instructionSet, dexoptNeeded, outputPath, - dexFlags, compilerFilter, volumeUuid, classLoaderContext, seInfo, downgrade, - targetSdkVersion, profileName, dexMetadataPath, compilationReason); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - - /** - * Enables or disables dex optimization blocking. - * - * <p> Enabling blocking will also involve cancelling pending dexopt call and killing child - * processes forked from installd to run dexopt. The pending dexopt call will return false - * when it is cancelled. - * - * @param block set to true to enable blocking / false to disable blocking. - */ - public void controlDexOptBlocking(boolean block) throws LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - try { - mInstalld.controlDexOptBlocking(block); - } catch (Exception e) { - Slog.w(TAG, "blockDexOpt failed", e); - } - } - - /** - * Analyzes the ART profiles of the given package, possibly merging the information - * into the reference profile. Returns whether or not we should optimize the package - * based on how much information is in the profile. - * - * @return one of {@link #PROFILE_ANALYSIS_OPTIMIZE}, - * {@link #PROFILE_ANALYSIS_DONT_OPTIMIZE_SMALL_DELTA}, - * {@link #PROFILE_ANALYSIS_DONT_OPTIMIZE_EMPTY_PROFILES} - */ - public int mergeProfiles(int uid, String packageName, String profileName) - throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return PROFILE_ANALYSIS_DONT_OPTIMIZE_SMALL_DELTA; - try { - return mInstalld.mergeProfiles(uid, packageName, profileName); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - - /** - * Dumps profiles associated with a package in a human readable format. - */ - public boolean dumpProfiles(int uid, String packageName, String profileName, String codePath, - boolean dumpClassesAndMethods) - throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return false; - BlockGuard.getVmPolicy().onPathAccess(codePath); - try { - return mInstalld.dumpProfiles(uid, packageName, profileName, codePath, - dumpClassesAndMethods); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - - public boolean copySystemProfile(String systemProfile, int uid, String packageName, - String profileName) throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return false; - try { - return mInstalld.copySystemProfile(systemProfile, uid, packageName, profileName); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - - public void rmdex(String codePath, String instructionSet) - throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - assertValidInstructionSet(instructionSet); - if (!checkBeforeRemote()) return; - BlockGuard.getVmPolicy().onPathAccess(codePath); - try { - mInstalld.rmdex(codePath, instructionSet); - } catch (Exception e) { - throw InstallerException.from(e); - } + throw new LegacyDexoptDisabledException(); } /** @@ -757,43 +625,6 @@ public class Installer extends SystemService { } } - public void clearAppProfiles(String packageName, String profileName) - throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return; - try { - mInstalld.clearAppProfiles(packageName, profileName); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - - public void destroyAppProfiles(String packageName) - throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return; - try { - mInstalld.destroyAppProfiles(packageName); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - - /** - * Deletes the reference profile with the given name of the given package. - * @throws InstallerException if the deletion fails. - */ - public void deleteReferenceProfile(String packageName, String profileName) - throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return; - try { - mInstalld.deleteReferenceProfile(packageName, profileName); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - public void createUserData(String uuid, int userId, int userSerial, int flags) throws InstallerException { if (!checkBeforeRemote()) return; @@ -889,40 +720,6 @@ public class Installer extends SystemService { } } - /** - * Deletes the optimized artifacts generated by ART and returns the number - * of freed bytes. - */ - public long deleteOdex(String packageName, String apkPath, String instructionSet, - String outputPath) throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return -1; - BlockGuard.getVmPolicy().onPathAccess(apkPath); - BlockGuard.getVmPolicy().onPathAccess(outputPath); - try { - return mInstalld.deleteOdex(packageName, apkPath, instructionSet, outputPath); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - - public boolean reconcileSecondaryDexFile(String apkPath, String packageName, int uid, - String[] isas, @Nullable String volumeUuid, int flags) - throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - for (int i = 0; i < isas.length; i++) { - assertValidInstructionSet(isas[i]); - } - if (!checkBeforeRemote()) return false; - BlockGuard.getVmPolicy().onPathAccess(apkPath); - try { - return mInstalld.reconcileSecondaryDexFile(apkPath, packageName, uid, isas, - volumeUuid, flags); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - public byte[] hashSecondaryDexFile(String dexPath, String packageName, int uid, @Nullable String volumeUuid, int flags) throws InstallerException { if (!checkBeforeRemote()) return new byte[0]; @@ -934,28 +731,6 @@ public class Installer extends SystemService { } } - public boolean createProfileSnapshot(int appId, String packageName, String profileName, - String classpath) throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return false; - try { - return mInstalld.createProfileSnapshot(appId, packageName, profileName, classpath); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - - public void destroyProfileSnapshot(String packageName, String profileName) - throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return; - try { - mInstalld.destroyProfileSnapshot(packageName, profileName); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - public void invalidateMounts() throws InstallerException { if (!checkBeforeRemote()) return; try { @@ -999,30 +774,6 @@ public class Installer extends SystemService { } /** - * Prepares the app profile for the package at the given path: - * <ul> - * <li>Creates the current profile for the given user ID, unless the user ID is - * {@code UserHandle.USER_NULL}.</li> - * <li>Merges the profile from the dex metadata file (if present) into the reference - * profile.</li> - * </ul> - */ - public boolean prepareAppProfile(String pkg, @UserIdInt int userId, @AppIdInt int appId, - String profileName, String codePath, String dexMetadataPath) - throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return false; - BlockGuard.getVmPolicy().onPathAccess(codePath); - BlockGuard.getVmPolicy().onPathAccess(dexMetadataPath); - try { - return mInstalld.prepareAppProfile(pkg, userId, appId, profileName, codePath, - dexMetadataPath); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - - /** * Snapshots user data of the given package. * * @param pkg name of the package to snapshot user data for. @@ -1152,34 +903,6 @@ public class Installer extends SystemService { } /** - * Returns the visibility of the optimized artifacts. - * - * @param packageName name of the package. - * @param apkPath path to the APK. - * @param instructionSet instruction set of the optimized artifacts. - * @param outputPath path to the directory that contains the optimized artifacts (i.e., the - * directory that {@link #dexopt} outputs to). - * - * @return {@link #ODEX_NOT_FOUND} if the optimized artifacts are not found, or - * {@link #ODEX_IS_PUBLIC} if the optimized artifacts are accessible by all apps, or - * {@link #ODEX_IS_PRIVATE} if the optimized artifacts are only accessible by this app. - * - * @throws InstallerException if failed to get the visibility of the optimized artifacts. - */ - public int getOdexVisibility(String packageName, String apkPath, String instructionSet, - String outputPath) throws InstallerException, LegacyDexoptDisabledException { - checkLegacyDexoptDisabled(); - if (!checkBeforeRemote()) return -1; - BlockGuard.getVmPolicy().onPathAccess(apkPath); - BlockGuard.getVmPolicy().onPathAccess(outputPath); - try { - return mInstalld.getOdexVisibility(packageName, apkPath, instructionSet, outputPath); - } catch (Exception e) { - throw InstallerException.from(e); - } - } - - /** * Returns an auth token for the provided writable FD. * * @param authFd a file descriptor to proof that the caller can write to the file. @@ -1247,14 +970,4 @@ public class Installer extends SystemService { super("Invalid call to legacy dexopt method while ART Service is in use."); } } - - /** - * Throws LegacyDexoptDisabledException if ART Service should be used instead of the - * {@link android.os.IInstalld} method that follows this method call. - */ - public static void checkLegacyDexoptDisabled() throws LegacyDexoptDisabledException { - if (useArtService()) { - throw new LegacyDexoptDisabledException(); - } - } } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index c6bb99eed7ee..20b669b96609 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -18,12 +18,12 @@ package com.android.server.pm; import static android.Manifest.permission.READ_FRAME_BUFFER; import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.AppOpsManager.OP_ARCHIVE_ICON_OVERLAY; import static android.app.AppOpsManager.OP_UNARCHIVAL_CONFIRMATION; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_MUTABLE; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; @@ -555,12 +555,6 @@ public class LauncherAppsService extends SystemService { return false; } - if (!mRoleManager - .getRoleHoldersAsUser( - RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid)) - .contains(callingPackage.getPackageName())) { - return false; - } if (mContext.checkPermission( Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL, callingPid, @@ -569,6 +563,13 @@ public class LauncherAppsService extends SystemService { return true; } + if (!mRoleManager + .getRoleHoldersAsUser( + RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid)) + .contains(callingPackage.getPackageName())) { + return false; + } + // TODO(b/321988638): add option to disable with a flag return mContext.checkPermission( android.Manifest.permission.ACCESS_HIDDEN_PROFILES, diff --git a/services/core/java/com/android/server/pm/OWNERS b/services/core/java/com/android/server/pm/OWNERS index c8bc56ce7dcd..85aee8606bc2 100644 --- a/services/core/java/com/android/server/pm/OWNERS +++ b/services/core/java/com/android/server/pm/OWNERS @@ -11,7 +11,6 @@ per-file StagingManager.java = dariofreni@google.com, ioffe@google.com, olilan@g # dex per-file AbstractStatsBase.java = file:dex/OWNERS -per-file BackgroundDexOptService.java = file:dex/OWNERS per-file CompilerStats.java = file:dex/OWNERS per-file DexOptHelper.java = file:dex/OWNERS per-file DynamicCodeLoggingService.java = file:dex/OWNERS diff --git a/services/core/java/com/android/server/pm/OtaDexoptService.java b/services/core/java/com/android/server/pm/OtaDexoptService.java index ea082cf77987..5b326fd297cb 100644 --- a/services/core/java/com/android/server/pm/OtaDexoptService.java +++ b/services/core/java/com/android/server/pm/OtaDexoptService.java @@ -16,7 +16,6 @@ package com.android.server.pm; -import static com.android.server.pm.DexOptHelper.useArtService; import static com.android.server.pm.InstructionSets.getAppDexInstructionSets; import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; @@ -305,13 +304,10 @@ public class OtaDexoptService extends IOtaDexopt.Stub { throws InstallerException { final StringBuilder builder = new StringBuilder(); - if (useArtService()) { - if ((dexFlags & DEXOPT_SECONDARY_DEX) != 0) { - // installd may change the reference profile in place for secondary dex - // files, which isn't safe with the lock free approach in ART Service. - throw new IllegalArgumentException( - "Invalid OTA dexopt call for secondary dex"); - } + if ((dexFlags & DEXOPT_SECONDARY_DEX) != 0) { + // installd may change the reference profile in place for secondary dex + // files, which isn't safe with the lock free approach in ART Service. + throw new IllegalArgumentException("Invalid OTA dexopt call for secondary dex"); } // The current version. For v10, see b/115993344. diff --git a/services/core/java/com/android/server/pm/PackageDexOptimizer.java b/services/core/java/com/android/server/pm/PackageDexOptimizer.java index 8a4080ff029d..396fa22393e4 100644 --- a/services/core/java/com/android/server/pm/PackageDexOptimizer.java +++ b/services/core/java/com/android/server/pm/PackageDexOptimizer.java @@ -18,7 +18,6 @@ package com.android.server.pm; import static android.content.pm.ApplicationInfo.HIDDEN_API_ENFORCEMENT_DISABLED; -import static com.android.server.pm.DexOptHelper.useArtService; import static com.android.server.pm.Installer.DEXOPT_BOOTCOMPLETE; import static com.android.server.pm.Installer.DEXOPT_DEBUGGABLE; import static com.android.server.pm.Installer.DEXOPT_ENABLE_HIDDEN_API_CHECKS; @@ -53,7 +52,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.SharedLibraryInfo; import android.content.pm.dex.ArtManager; import android.content.pm.dex.DexMetadataHelper; -import android.os.FileUtils; import android.os.PowerManager; import android.os.SystemClock; import android.os.SystemProperties; @@ -67,7 +65,6 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.F2fsUtils; -import com.android.internal.util.IndentingPrintWriter; import com.android.server.LocalServices; import com.android.server.apphibernation.AppHibernationManagerInternal; import com.android.server.pm.Installer.InstallerException; @@ -92,7 +89,6 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.Random; /** @@ -130,9 +126,8 @@ public class PackageDexOptimizer { private final Object mInstallLock; /** - * This should be accessed only through {@link #getInstallerLI()} with {@link #mInstallLock} - * or {@link #getInstallerWithoutLock()} without the lock. Check both methods for further - * details on when to use each of them. + * This should be accessed only through {@link #getInstallerLI()} with + * {@link #mInstallLock}. */ private final Installer mInstaller; @@ -248,15 +243,6 @@ public class PackageDexOptimizer { } /** - * Cancels currently running dex optimization. - */ - void controlDexOptBlocking(boolean block) throws LegacyDexoptDisabledException { - // This method should not hold mInstallLock as cancelling should be possible while - // the lock is held by other thread running performDexOpt. - getInstallerWithoutLock().controlDexOptBlocking(block); - } - - /** * Performs dexopt on all code paths of the given package. * It assumes the install lock is held. */ @@ -334,7 +320,7 @@ public class PackageDexOptimizer { final boolean isUsedByOtherApps; if (options.isDexoptAsSharedLibrary()) { isUsedByOtherApps = true; - } else if (useArtService()) { + } else { // We get here when collecting dexopt commands in OTA preopt, even when ART Service // is in use. packageUseInfo isn't useful in that case since the legacy dex use // database hasn't been updated. So we'd have to query ART Service instead, but it @@ -342,8 +328,6 @@ public class PackageDexOptimizer { // That means such apps will get preopted wrong, and we'll leave it to a later // background dexopt after reboot instead. isUsedByOtherApps = false; - } else { - isUsedByOtherApps = packageUseInfo.isUsedByOtherApps(path); } String compilerFilter = getRealCompilerFilter(pkg, options.getCompilerFilter()); @@ -439,12 +423,10 @@ public class PackageDexOptimizer { } } } finally { + // ART Service is always enabled, so we should only arrive here + // during OTA preopt, and there should be no cloud profile. if (cloudProfileName != null) { - try { - mInstaller.deleteReferenceProfile(pkg.getPackageName(), cloudProfileName); - } catch (InstallerException e) { - Slog.w(TAG, "Failed to cleanup cloud profile", e); - } + throw new LegacyDexoptDisabledException(); } } } @@ -457,30 +439,15 @@ public class PackageDexOptimizer { * * @return true on success, or false otherwise. */ - @GuardedBy("mInstallLock") private boolean prepareCloudProfile(AndroidPackage pkg, String profileName, String path, @Nullable String dexMetadataPath) throws LegacyDexoptDisabledException { if (dexMetadataPath != null) { - if (mInstaller.isIsolated()) { - // If the installer is isolated, the two calls to it below will return immediately, - // so this only short-circuits that a bit. We need to do it to avoid the - // LegacyDexoptDisabledException getting thrown first, when we get here during OTA - // preopt and ART Service is enabled. - return true; - } - - try { - // Make sure we don't keep any existing contents. - mInstaller.deleteReferenceProfile(pkg.getPackageName(), profileName); - - final int appId = UserHandle.getAppId(pkg.getUid()); - mInstaller.prepareAppProfile(pkg.getPackageName(), UserHandle.USER_NULL, appId, - profileName, path, dexMetadataPath); - return true; - } catch (InstallerException e) { - Slog.w(TAG, "Failed to prepare cloud profile", e); - return false; + // ART Service is always enabled, so we should only arrive here + // during OTA preopt, i.e. when the installer is isolated. + if (!mInstaller.isIsolated()) { + throw new LegacyDexoptDisabledException(); } + return true; } else { return false; } @@ -554,37 +521,6 @@ public class PackageDexOptimizer { return getReasonName(compilationReason) + annotation; } - /** - * Performs dexopt on the secondary dex {@code path} belonging to the app {@code info}. - * - * @return - * DEX_OPT_FAILED if there was any exception during dexopt - * DEX_OPT_PERFORMED if dexopt was performed successfully on the given path. - * NOTE that DEX_OPT_PERFORMED for secondary dex files includes the case when the dex file - * didn't need an update. That's because at the moment we don't get more than success/failure - * from installd. - * - * TODO(calin): Consider adding return codes to installd dexopt invocation (rather than - * throwing exceptions). Or maybe make a separate call to installd to get DexOptNeeded, though - * that seems wasteful. - */ - @DexOptResult - public int dexOptSecondaryDexPath(ApplicationInfo info, String path, - PackageDexUsage.DexUseInfo dexUseInfo, DexoptOptions options) - throws LegacyDexoptDisabledException { - if (info.uid == -1) { - throw new IllegalArgumentException("Dexopt for path " + path + " has invalid uid."); - } - synchronized (mInstallLock) { - final long acquireTime = acquireWakeLockLI(info.uid); - try { - return dexOptSecondaryDexPathLI(info, path, dexUseInfo, options); - } finally { - releaseWakeLockLI(acquireTime); - } - } - } - @GuardedBy("mInstallLock") private long acquireWakeLockLI(final int uid) { // During boot the system doesn't need to instantiate and obtain a wake lock. @@ -618,69 +554,6 @@ public class PackageDexOptimizer { } } - @GuardedBy("mInstallLock") - @DexOptResult - private int dexOptSecondaryDexPathLI(ApplicationInfo info, String path, - PackageDexUsage.DexUseInfo dexUseInfo, DexoptOptions options) - throws LegacyDexoptDisabledException { - String compilerFilter = getRealCompilerFilter(info, options.getCompilerFilter(), - dexUseInfo.isUsedByOtherApps()); - // Get the dexopt flags after getRealCompilerFilter to make sure we get the correct flags. - // Secondary dex files are currently not compiled at boot. - int dexoptFlags = getDexFlags(info, compilerFilter, options) | DEXOPT_SECONDARY_DEX; - // Check the app storage and add the appropriate flags. - if (info.deviceProtectedDataDir != null && - FileUtils.contains(info.deviceProtectedDataDir, path)) { - dexoptFlags |= DEXOPT_STORAGE_DE; - } else if (info.credentialProtectedDataDir != null && - FileUtils.contains(info.credentialProtectedDataDir, path)) { - dexoptFlags |= DEXOPT_STORAGE_CE; - } else { - Slog.e(TAG, "Could not infer CE/DE storage for package " + info.packageName); - return DEX_OPT_FAILED; - } - String classLoaderContext = null; - if (dexUseInfo.isUnsupportedClassLoaderContext() - || dexUseInfo.isVariableClassLoaderContext()) { - // If we have an unknown (not yet set), or a variable class loader chain. Just verify - // the dex file. - compilerFilter = "verify"; - } else { - classLoaderContext = dexUseInfo.getClassLoaderContext(); - } - - int reason = options.getCompilationReason(); - Log.d(TAG, "Running dexopt on: " + path - + " pkg=" + info.packageName + " isa=" + dexUseInfo.getLoaderIsas() - + " reason=" + getReasonName(reason) - + " dexoptFlags=" + printDexoptFlags(dexoptFlags) - + " target-filter=" + compilerFilter - + " class-loader-context=" + classLoaderContext); - - try { - for (String isa : dexUseInfo.getLoaderIsas()) { - // Reuse the same dexopt path as for the primary apks. We don't need all the - // arguments as some (dexopNeeded and oatDir) will be computed by installd because - // system server cannot read untrusted app content. - // TODO(calin): maybe add a separate call. - boolean completed = getInstallerLI().dexopt(path, info.uid, info.packageName, - isa, /* dexoptNeeded= */ 0, - /* outputPath= */ null, dexoptFlags, - compilerFilter, info.volumeUuid, classLoaderContext, info.seInfo, - options.isDowngrade(), info.targetSdkVersion, /* profileName= */ null, - /* dexMetadataPath= */ null, getReasonName(reason)); - if (!completed) { - return DEX_OPT_CANCELLED; - } - } - - return DEX_OPT_PERFORMED; - } catch (InstallerException e) { - Slog.w(TAG, "Failed to dexopt", e); - return DEX_OPT_FAILED; - } - } - /** * Adjust the given dexopt-needed value. Can be overridden to influence the decision to * optimize or not (and in what way). @@ -697,59 +570,6 @@ public class PackageDexOptimizer { } /** - * Dumps the dexopt state of the given package {@code pkg} to the given {@code PrintWriter}. - */ - void dumpDexoptState(IndentingPrintWriter pw, AndroidPackage pkg, - PackageStateInternal pkgSetting, PackageDexUsage.PackageUseInfo useInfo) - throws LegacyDexoptDisabledException { - final String[] instructionSets = getAppDexInstructionSets(pkgSetting.getPrimaryCpuAbi(), - pkgSetting.getSecondaryCpuAbi()); - final String[] dexCodeInstructionSets = getDexCodeInstructionSets(instructionSets); - - final List<String> paths = AndroidPackageUtils.getAllCodePathsExcludingResourceOnly(pkg); - - for (String path : paths) { - pw.println("path: " + path); - pw.increaseIndent(); - - for (String isa : dexCodeInstructionSets) { - try { - DexFile.OptimizationInfo info = DexFile.getDexFileOptimizationInfo(path, isa); - pw.println(isa + ": [status=" + info.getStatus() - +"] [reason=" + info.getReason() + "]"); - } catch (IOException ioe) { - pw.println(isa + ": [Exception]: " + ioe.getMessage()); - } - } - - if (useInfo.isUsedByOtherApps(path)) { - pw.println("used by other apps: " + useInfo.getLoadingPackages(path)); - } - - Map<String, PackageDexUsage.DexUseInfo> dexUseInfoMap = useInfo.getDexUseInfoMap(); - - if (!dexUseInfoMap.isEmpty()) { - pw.println("known secondary dex files:"); - pw.increaseIndent(); - for (Map.Entry<String, PackageDexUsage.DexUseInfo> e : dexUseInfoMap.entrySet()) { - String dex = e.getKey(); - PackageDexUsage.DexUseInfo dexUseInfo = e.getValue(); - pw.println(dex); - pw.increaseIndent(); - // TODO(calin): get the status of the oat file (needs installd call) - pw.println("class loader context: " + dexUseInfo.getClassLoaderContext()); - if (dexUseInfo.isUsedByOtherApps()) { - pw.println("used by other apps: " + dexUseInfo.getLoadingPackages()); - } - pw.decreaseIndent(); - } - pw.decreaseIndent(); - } - pw.decreaseIndent(); - } - } - - /** * Returns the compiler filter that should be used to optimize the secondary dex. * The target filter will be updated if the package code is used by other apps * or if it has the safe mode flag set. @@ -898,14 +718,13 @@ public class PackageDexOptimizer { * Assesses if there's a need to perform dexopt on {@code path} for the given * configuration (isa, compiler filter, profile). */ - @GuardedBy("mInstallLock") private int getDexoptNeeded(String packageName, String path, String isa, String compilerFilter, String classLoaderContext, int profileAnalysisResult, boolean downgrade, int dexoptFlags, String oatDir) throws LegacyDexoptDisabledException { // Allow calls from OtaDexoptService even when ART Service is in use. The installer is // isolated in that case so later calls to it won't call into installd anyway. if (!mInstaller.isIsolated()) { - Installer.checkLegacyDexoptDisabled(); + throw new LegacyDexoptDisabledException(); } final boolean shouldBePublic = (dexoptFlags & DEXOPT_PUBLIC) != 0; @@ -953,16 +772,9 @@ public class PackageDexOptimizer { } /** Returns true if the current artifacts of the app are private to the app itself. */ - @GuardedBy("mInstallLock") private boolean isOdexPrivate(String packageName, String path, String isa, String oatDir) throws LegacyDexoptDisabledException { - try { - return mInstaller.getOdexVisibility(packageName, path, isa, oatDir) - == Installer.ODEX_IS_PRIVATE; - } catch (InstallerException e) { - Slog.w(TAG, "Failed to get odex visibility for " + path, e); - return false; - } + throw new LegacyDexoptDisabledException(); } /** @@ -976,22 +788,7 @@ public class PackageDexOptimizer { */ private int analyseProfiles(AndroidPackage pkg, int uid, String profileName, String compilerFilter) throws LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); - - // Check if we are allowed to merge and if the compiler filter is profile guided. - if (!isProfileGuidedCompilerFilter(compilerFilter)) { - return PROFILE_ANALYSIS_DONT_OPTIMIZE_SMALL_DELTA; - } - // Merge profiles. It returns whether or not there was an updated in the profile info. - try { - synchronized (mInstallLock) { - return getInstallerLI().mergeProfiles(uid, pkg.getPackageName(), profileName); - } - } catch (InstallerException e) { - Slog.w(TAG, "Failed to merge profiles", e); - // We don't need to optimize if we failed to merge. - return PROFILE_ANALYSIS_DONT_OPTIMIZE_SMALL_DELTA; - } + throw new LegacyDexoptDisabledException(); } /** @@ -1101,7 +898,7 @@ public class PackageDexOptimizer { /** * Returns {@link #mInstaller} with {@link #mInstallLock}. This should be used for all - * {@link #mInstaller} access unless {@link #getInstallerWithoutLock()} is allowed. + * {@link #mInstaller} access. */ @GuardedBy("mInstallLock") private Installer getInstallerLI() { @@ -1109,14 +906,6 @@ public class PackageDexOptimizer { } /** - * Returns {@link #mInstaller} without lock. This should be used only inside - * {@link #controlDexOptBlocking(boolean)}. - */ - private Installer getInstallerWithoutLock() { - return mInstaller; - } - - /** * Injector for {@link PackageDexOptimizer} dependencies */ interface Injector { diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java index 8da168375447..7a72e70592d3 100644 --- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java +++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java @@ -16,6 +16,7 @@ package com.android.server.pm; +import static android.app.admin.flags.Flags.crossUserSuspensionEnabled; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; import static android.content.pm.PackageManager.RESTRICTION_NONE; @@ -45,6 +46,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Process; +import android.os.UserHandle; import android.os.storage.StorageManager; import android.util.ArrayMap; import android.util.ArraySet; @@ -687,14 +689,17 @@ abstract class PackageManagerInternalBase extends PackageManagerInternal { @Override @Deprecated public final void unsuspendAdminSuspendedPackages(int affectedUser) { - final int suspendingUserId = affectedUser; - mService.unsuspendForSuspendingPackage(snapshot(), PLATFORM_PACKAGE_NAME, suspendingUserId); + final int suspendingUserId = + crossUserSuspensionEnabled() ? UserHandle.USER_SYSTEM : affectedUser; + mService.unsuspendForSuspendingPackage( + snapshot(), PLATFORM_PACKAGE_NAME, suspendingUserId, /* inAllUsers= */ false); } @Override @Deprecated public final boolean isAdminSuspendingAnyPackages(int userId) { - final int suspendingUserId = userId; + final int suspendingUserId = + crossUserSuspensionEnabled() ? UserHandle.USER_SYSTEM : userId; return snapshot().isSuspendingAnyPackages(PLATFORM_PACKAGE_NAME, suspendingUserId, userId); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 35cb5b000219..9a2b98f316c4 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -18,6 +18,7 @@ package com.android.server.pm; import static android.Manifest.permission.MANAGE_DEVICE_ADMINS; import static android.Manifest.permission.SET_HARMFUL_APP_WARNINGS; import static android.app.AppOpsManager.MODE_IGNORED; +import static android.app.admin.flags.Flags.crossUserSuspensionEnabled; import static android.content.pm.PackageManager.APP_METADATA_SOURCE_APK; import static android.content.pm.PackageManager.APP_METADATA_SOURCE_UNKNOWN; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; @@ -41,9 +42,6 @@ import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE; import static com.android.internal.annotations.VisibleForTesting.Visibility; import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_DURATION__EVENT__OTA_PACKAGE_MANAGER_INIT_TIME; -import static com.android.server.pm.DexOptHelper.useArtService; -import static com.android.server.pm.InstructionSets.getDexCodeInstructionSet; -import static com.android.server.pm.InstructionSets.getPreferredInstructionSet; import static com.android.server.pm.PackageManagerServiceUtils.compareSignatures; import static com.android.server.pm.PackageManagerServiceUtils.isInstalledByAdb; import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo; @@ -216,10 +214,8 @@ import com.android.server.art.model.DeleteResult; import com.android.server.compat.CompatChange; import com.android.server.compat.PlatformCompat; import com.android.server.pm.Installer.InstallerException; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.Settings.VersionInfo; import com.android.server.pm.dex.ArtManagerService; -import com.android.server.pm.dex.ArtUtils; import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.DynamicCodeLogger; import com.android.server.pm.local.PackageManagerLocalImpl; @@ -820,8 +816,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService // TODO(b/260124949): Remove these. final PackageDexOptimizer mPackageDexOptimizer; - @Nullable - final BackgroundDexOptService mBackgroundDexOptService; // null when ART Service is in use. // DexManager handles the usage of dex files (e.g. secondary files, whether or not a package // is used by other apps). private final DexManager mDexManager; @@ -1763,16 +1757,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService new DefaultSystemWrapper(), LocalServices::getService, context::getSystemService, - (i, pm) -> { - if (useArtService()) { - return null; - } - try { - return new BackgroundDexOptService(i.getContext(), i.getDexManager(), pm); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - }, (i, pm) -> IBackupManager.Stub.asInterface(ServiceManager.getService( Context.BACKUP_SERVICE)), (i, pm) -> new SharedLibrariesImpl(pm, i), @@ -1916,7 +1900,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService mApexManager = testParams.apexManager; mArtManagerService = testParams.artManagerService; mAvailableFeatures = testParams.availableFeatures; - mBackgroundDexOptService = testParams.backgroundDexOptService; mDefParseFlags = testParams.defParseFlags; mDefaultAppProvider = testParams.defaultAppProvider; mLegacyPermissionManager = testParams.legacyPermissionManagerInternal; @@ -2113,7 +2096,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService mPackageDexOptimizer = injector.getPackageDexOptimizer(); mDexManager = injector.getDexManager(); mDynamicCodeLogger = injector.getDynamicCodeLogger(); - mBackgroundDexOptService = injector.getBackgroundDexOptService(); mArtManagerService = injector.getArtManagerService(); mMoveCallbacks = new MovePackageHelper.MoveCallbacks(FgThread.get().getLooper()); mSharedLibraries = mInjector.getSharedLibrariesImpl(); @@ -2369,19 +2351,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService null /*scannedPackage*/, mInjector.getAbiHelper().getAdjustedAbiForSharedUser( setting.getPackageStates(), null /*scannedPackage*/)); - if (!useArtService() && // Skip for ART Service since it has its own dex file GC. - changedAbiCodePath != null && changedAbiCodePath.size() > 0) { - for (int i = changedAbiCodePath.size() - 1; i >= 0; --i) { - final String codePathString = changedAbiCodePath.get(i); - try { - mInstaller.rmdex(codePathString, - getDexCodeInstructionSet(getPreferredInstructionSet())); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } catch (InstallerException ignored) { - } - } - } // Adjust seInfo to ensure apps which share a sharedUserId are placed in the same // SELinux domain. setting.fixSeInfoLocked(); @@ -3213,27 +3182,53 @@ public class PackageManagerService implements PackageSender, TestUtilityService callingMethod); } - final int packageUid = snapshot.getPackageUid(suspender.packageName, 0, targetUserId); - final boolean allowedPackageUid = packageUid == callingUid; - // TODO(b/139383163): remove special casing for shell and enforce INTERACT_ACROSS_USERS_FULL - final boolean allowedShell = callingUid == SHELL_UID - && UserHandle.isSameApp(packageUid, callingUid); + if (crossUserSuspensionEnabled()) { + final int suspendingPackageUid = + snapshot.getPackageUid(suspender.packageName, 0, suspender.userId); + if (suspendingPackageUid != callingUid) { + throw new SecurityException("Suspender package %s doesn't match calling uid %d" + .formatted(suspender.packageName, callingUid)); + } + if (targetUserId != suspender.userId) { + mContext.enforceCallingOrSelfPermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL, callingMethod); + } + } else { + // Here only SHELL can suspend across users + final int packageUid = + snapshot.getPackageUid(suspender.packageName, 0, targetUserId); + final boolean allowedPackageUid = packageUid == callingUid; + final boolean allowedShell = callingUid == SHELL_UID + && UserHandle.isSameApp(packageUid, callingUid); - if (!allowedShell && !allowedPackageUid) { - throw new SecurityException("Suspending package " + suspender.packageName - + " in user " + targetUserId + " does not belong to calling uid " + callingUid); + if (!allowedShell && !allowedPackageUid) { + throw new SecurityException("Suspending package " + suspender.packageName + + " in user " + targetUserId + " does not belong to calling uid " + + callingUid); + } } } + /** + * @param inAllUsers Whether to unsuspend packages suspended by the given package in other + * users. This flag is only used when cross-user suspension is enabled. + */ void unsuspendForSuspendingPackage(@NonNull Computer computer, String suspendingPackage, - @UserIdInt int suspendingUserId) { + @UserIdInt int suspendingUserId, boolean inAllUsers) { // TODO: This can be replaced by a special parameter to iterate all packages, rather than // this weird pre-collect of all packages. final String[] allPackages = computer.getPackageStates().keySet().toArray(new String[0]); final Predicate<UserPackage> suspenderPredicate = UserPackage.of(suspendingUserId, suspendingPackage)::equals; - mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer, - allPackages, suspenderPredicate, suspendingUserId); + if (!crossUserSuspensionEnabled() || !inAllUsers) { + mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer, + allPackages, suspenderPredicate, suspendingUserId); + } else { + for (int targetUserId: mUserManager.getUserIds()) { + mSuspendPackageHelper.removeSuspensionsBySuspendingPackage( + computer, allPackages, suspenderPredicate, targetUserId); + } + } } void removeAllDistractingPackageRestrictions(@NonNull Computer snapshot, int userId) { @@ -4085,7 +4080,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService // This app should not generally be allowed to get disabled by the UI, but // if it ever does, we don't want to end up with some of the user's apps // permanently suspended. - unsuspendForSuspendingPackage(computer, packageName, userId); + unsuspendForSuspendingPackage(computer, packageName, userId, true /* inAllUsers */); removeAllDistractingPackageRestrictions(computer, userId); } success = true; @@ -4309,16 +4304,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService } }); - if (!useArtService()) { - // The background dexopt job is scheduled in DexOptHelper.initializeArtManagerLocal when - // ART Service is in use. - try { - mBackgroundDexOptService.systemReady(); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - } - // Prune unused static shared libraries which have been cached a period of time schedulePruneUnusedStaticSharedLibraries(false /* delay */); @@ -4381,6 +4366,19 @@ public class PackageManagerService implements PackageSender, TestUtilityService } mInstantAppRegistry.onUserRemoved(userId); mPackageMonitorCallbackHelper.onUserRemoved(userId); + if (crossUserSuspensionEnabled()) { + cleanUpCrossUserSuspension(userId); + } + } + + private void cleanUpCrossUserSuspension(int removedUser) { + final Computer computer = snapshotComputer(); + var allPackages = computer.getAllAvailablePackageNames(); + for (int targetUserId : mUserManager.getUserIds()) { + if (targetUserId == removedUser) continue; + mSuspendPackageHelper.removeSuspensionsBySuspendingPackage(computer, allPackages, + userPackage -> userPackage.userId == removedUser, targetUserId); + } } /** @@ -4787,7 +4785,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService if (checkPermission(Manifest.permission.SUSPEND_APPS, packageName, userId) == PERMISSION_GRANTED) { final Computer snapshot = snapshotComputer(); - unsuspendForSuspendingPackage(snapshot, packageName, userId); + unsuspendForSuspendingPackage( + snapshot, packageName, userId, true /* inAllUsers */); removeAllDistractingPackageRestrictions(snapshot, userId); synchronized (mLock) { flushPackageRestrictionsAsUserInternalLocked(userId); @@ -6281,7 +6280,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService final boolean quarantined = ((flags & PackageManager.FLAG_SUSPEND_QUARANTINED) != 0) && Flags.quarantinedEnabled(); final Computer snapshot = snapshotComputer(); - final UserPackage suspender = UserPackage.of(targetUserId, suspendingPackage); + final UserPackage suspender = crossUserSuspensionEnabled() + ? UserPackage.of(suspendingUserId, suspendingPackage) + : UserPackage.of(targetUserId, suspendingPackage); enforceCanSetPackagesSuspendedAsUser(snapshot, quarantined, suspender, callingUid, targetUserId, "setPackagesSuspendedAsUser"); return mSuspendPackageHelper.setPackagesSuspended(snapshot, packageNames, suspended, @@ -6749,7 +6750,10 @@ public class PackageManagerService implements PackageSender, TestUtilityService @Override public String[] setPackagesSuspendedByAdmin( @UserIdInt int userId, @NonNull String[] packageNames, boolean suspended) { - final int suspendingUserId = userId; + // Suspension by admin isn't attributed to admin package but to the platform, + // Using USER_SYSTEM for consistency with other internal suspenders, like shell or root. + final int suspendingUserId = + crossUserSuspensionEnabled() ? UserHandle.USER_SYSTEM : userId; final UserPackage suspender = UserPackage.of( suspendingUserId, PackageManagerService.PLATFORM_PACKAGE_NAME); return mSuspendPackageHelper.setPackagesSuspended(snapshotComputer(), packageNames, @@ -6903,46 +6907,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService } } - /** @deprecated For legacy shell command only. */ - @Override - @Deprecated - public void legacyDumpProfiles(String packageName, boolean dumpClassesAndMethods) - throws LegacyDexoptDisabledException { - final Computer snapshot = snapshotComputer(); - AndroidPackage pkg = snapshot.getPackage(packageName); - if (pkg == null) { - throw new IllegalArgumentException("Unknown package: " + packageName); - } - - synchronized (mInstallLock) { - Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "dump profiles"); - mArtManagerService.dumpProfiles(pkg, dumpClassesAndMethods); - Trace.traceEnd(Trace.TRACE_TAG_DALVIK); - } - } - - /** @deprecated For legacy shell command only. */ - @Override - @Deprecated - public void legacyForceDexOpt(String packageName) throws LegacyDexoptDisabledException { - mDexOptHelper.forceDexOpt(snapshotComputer(), packageName); - } - - /** @deprecated For legacy shell command only. */ - @Override - @Deprecated - public void legacyReconcileSecondaryDexFiles(String packageName) - throws LegacyDexoptDisabledException { - final Computer snapshot = snapshotComputer(); - if (snapshot.getInstantAppPackageName(Binder.getCallingUid()) != null) { - return; - } else if (snapshot.isInstantAppInternal( - packageName, UserHandle.getCallingUserId(), Process.SYSTEM_UID)) { - return; - } - mDexManager.reconcileSecondaryDexFiles(packageName); - } - @Override @SuppressWarnings("GuardedBy") public void updateRuntimePermissionsFingerprint(@UserIdInt int userId) { @@ -7512,33 +7476,20 @@ public class PackageManagerService implements PackageSender, TestUtilityService PackageManagerServiceUtils.enforceSystemOrRootOrShell( "Only the system or shell can delete oat artifacts"); - if (DexOptHelper.useArtService()) { - // TODO(chiuwinson): Retrieve filtered snapshot from Computer instance instead. - try (PackageManagerLocal.FilteredSnapshot filteredSnapshot = - PackageManagerServiceUtils.getPackageManagerLocal() - .withFilteredSnapshot()) { - try { - DeleteResult res = DexOptHelper.getArtManagerLocal().deleteDexoptArtifacts( - filteredSnapshot, packageName); - return res.getFreedBytes(); - } catch (IllegalArgumentException e) { - Log.e(TAG, e.toString()); - return -1; - } catch (IllegalStateException e) { - Slog.wtfStack(TAG, e.toString()); - return -1; - } - } - } else { - PackageStateInternal packageState = snapshot.getPackageStateInternal(packageName); - if (packageState == null || packageState.getPkg() == null) { - return -1; // error code of deleteOptimizedFiles - } + // TODO(chiuwinson): Retrieve filtered snapshot from Computer instance instead. + try (PackageManagerLocal.FilteredSnapshot filteredSnapshot = + PackageManagerServiceUtils.getPackageManagerLocal() + .withFilteredSnapshot()) { try { - return mDexManager.deleteOptimizedFiles( - ArtUtils.createArtPackageInfo(packageState.getPkg(), packageState)); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); + DeleteResult res = DexOptHelper.getArtManagerLocal().deleteDexoptArtifacts( + filteredSnapshot, packageName); + return res.getFreedBytes(); + } catch (IllegalArgumentException e) { + Log.e(TAG, e.toString()); + return -1; + } catch (IllegalStateException e) { + Slog.wtfStack(TAG, e.toString()); + return -1; } } } diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java index 049737d42f51..83f3b16b31d1 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java @@ -16,7 +16,6 @@ package com.android.server.pm; -import android.annotation.Nullable; import android.app.ActivityManagerInternal; import android.app.backup.IBackupManager; import android.content.ComponentName; @@ -138,8 +137,6 @@ public class PackageManagerServiceInjector { private final Singleton<DomainVerificationManagerInternal> mDomainVerificationManagerInternalProducer; private final Singleton<Handler> mHandlerProducer; - private final Singleton<BackgroundDexOptService> - mBackgroundDexOptService; // TODO(b/260124949): Remove this. private final Singleton<IBackupManager> mIBackupManager; private final Singleton<SharedLibrariesImpl> mSharedLibrariesProducer; private final Singleton<CrossProfileIntentFilterHelper> mCrossProfileIntentFilterHelperProducer; @@ -180,7 +177,6 @@ public class PackageManagerServiceInjector { SystemWrapper systemWrapper, ServiceProducer getLocalServiceProducer, ServiceProducer getSystemServiceProducer, - Producer<BackgroundDexOptService> backgroundDexOptService, Producer<IBackupManager> iBackupManager, Producer<SharedLibrariesImpl> sharedLibrariesProducer, Producer<CrossProfileIntentFilterHelper> crossProfileIntentFilterHelperProducer, @@ -234,7 +230,6 @@ public class PackageManagerServiceInjector { new Singleton<>( domainVerificationManagerInternalProducer); mHandlerProducer = new Singleton<>(handlerProducer); - mBackgroundDexOptService = new Singleton<>(backgroundDexOptService); mIBackupManager = new Singleton<>(iBackupManager); mSharedLibrariesProducer = new Singleton<>(sharedLibrariesProducer); mCrossProfileIntentFilterHelperProducer = new Singleton<>( @@ -409,11 +404,6 @@ public class PackageManagerServiceInjector { return getLocalService(ActivityManagerInternal.class); } - @Nullable - public BackgroundDexOptService getBackgroundDexOptService() { - return mBackgroundDexOptService.get(this, mPackageManager); - } - public IBackupManager getIBackupManager() { return mIBackupManager.get(this, mPackageManager); } diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java index 2d797187b7f1..289373ee1456 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java @@ -105,7 +105,6 @@ public final class PackageManagerServiceTestParams { public boolean isEngBuild; public boolean isUserDebugBuild; public int sdkInt = Build.VERSION.SDK_INT; - public @Nullable BackgroundDexOptService backgroundDexOptService; public final String incrementalVersion = Build.VERSION.INCREMENTAL; public BroadcastHelper broadcastHelper; public AppDataHelper appDataHelper; diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 4fb9b56a5f5f..a9e1725ea9a0 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -29,7 +29,6 @@ import static android.content.pm.PackageManager.RESTRICTION_NONE; import static com.android.server.LocalManagerRegistry.ManagerNotFoundException; import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; -import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import android.accounts.IAccountManager; import android.annotation.NonNull; @@ -67,7 +66,6 @@ import android.content.pm.SharedLibraryInfo; import android.content.pm.SuspendDialogInfo; import android.content.pm.UserInfo; import android.content.pm.VersionedPackage; -import android.content.pm.dex.ArtManager; import android.content.pm.dex.DexMetadataHelper; import android.content.pm.dex.ISnapshotRuntimeProfileCallback; import android.content.pm.parsing.ApkLite; @@ -102,8 +100,6 @@ import android.os.UserManager; import android.os.incremental.V4Signature; import android.os.storage.StorageManager; import android.permission.PermissionManager; -import android.system.ErrnoException; -import android.system.Os; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.ArrayMap; @@ -123,25 +119,20 @@ import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; import com.android.server.SystemConfig; import com.android.server.art.ArtManagerLocal; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.PackageManagerShellCommandDataLoader.Metadata; import com.android.server.pm.permission.LegacyPermissionManagerInternal; import com.android.server.pm.permission.PermissionAllowlist; import com.android.server.pm.verify.domain.DomainVerificationShell; -import dalvik.system.DexFile; - import libcore.io.IoUtils; import libcore.io.Streams; import libcore.util.HexEncoding; import java.io.BufferedReader; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.io.OutputStream; import java.io.PrintWriter; import java.net.URISyntaxException; import java.security.SecureRandom; @@ -154,7 +145,6 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.CompletableFuture; @@ -400,15 +390,7 @@ class PackageManagerShellCommand extends ShellCommand { return runGetDomainVerificationAgent(); default: { if (ART_SERVICE_COMMANDS.contains(cmd)) { - if (DexOptHelper.useArtService()) { - return runArtServiceCommand(); - } else { - try { - return runLegacyDexoptCommand(cmd); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - } + return runArtServiceCommand(); } Boolean domainVerificationResult = @@ -438,40 +420,6 @@ class PackageManagerShellCommand extends ShellCommand { return -1; } - private int runLegacyDexoptCommand(@NonNull String cmd) - throws RemoteException, LegacyDexoptDisabledException { - Installer.checkLegacyDexoptDisabled(); - - if (!PackageManagerServiceUtils.isRootOrShell(Binder.getCallingUid())) { - throw new SecurityException("Dexopt shell commands need root or shell access"); - } - - switch (cmd) { - case "compile": - return runCompile(); - case "reconcile-secondary-dex-files": - return runreconcileSecondaryDexFiles(); - case "force-dex-opt": - return runForceDexOpt(); - case "bg-dexopt-job": - return runBgDexOpt(); - case "cancel-bg-dexopt-job": - return cancelBgDexOptJob(); - case "delete-dexopt": - return runDeleteDexOpt(); - case "dump-profiles": - return runDumpProfiles(); - case "snapshot-profile": - return runSnapshotProfile(); - case "art": - getOutPrintWriter().println("ART Service not enabled"); - return -1; - default: - // Can't happen. - throw new IllegalArgumentException(); - } - } - /** * Shows module info * @@ -2067,340 +2015,6 @@ class PackageManagerShellCommand extends ShellCommand { } } - private int runCompile() throws RemoteException { - final PrintWriter pw = getOutPrintWriter(); - boolean forceCompilation = false; - boolean allPackages = false; - boolean clearProfileData = false; - String compilerFilter = null; - String compilationReason = null; - boolean secondaryDex = false; - String split = null; - - String opt; - while ((opt = getNextOption()) != null) { - switch (opt) { - case "-a": - allPackages = true; - break; - case "-c": - clearProfileData = true; - break; - case "-f": - forceCompilation = true; - break; - case "-m": - compilerFilter = getNextArgRequired(); - break; - case "-r": - compilationReason = getNextArgRequired(); - break; - case "--check-prof": - getNextArgRequired(); - pw.println("Warning: Ignoring obsolete flag --check-prof " - + "- it is unconditionally enabled now"); - break; - case "--reset": - forceCompilation = true; - clearProfileData = true; - compilationReason = "install"; - break; - case "--secondary-dex": - secondaryDex = true; - break; - case "--split": - split = getNextArgRequired(); - break; - default: - pw.println("Error: Unknown option: " + opt); - return 1; - } - } - - final boolean compilerFilterGiven = compilerFilter != null; - final boolean compilationReasonGiven = compilationReason != null; - // Make sure exactly one of -m, or -r is given. - if (compilerFilterGiven && compilationReasonGiven) { - pw.println("Cannot use compilation filter (\"-m\") and compilation reason (\"-r\") " - + "at the same time"); - return 1; - } - if (!compilerFilterGiven && !compilationReasonGiven) { - pw.println("Cannot run without any of compilation filter (\"-m\") and compilation " - + "reason (\"-r\")"); - return 1; - } - - if (allPackages && split != null) { - pw.println("-a cannot be specified together with --split"); - return 1; - } - - if (secondaryDex && split != null) { - pw.println("--secondary-dex cannot be specified together with --split"); - return 1; - } - - String targetCompilerFilter = null; - if (compilerFilterGiven) { - if (!DexFile.isValidCompilerFilter(compilerFilter)) { - pw.println("Error: \"" + compilerFilter + - "\" is not a valid compilation filter."); - return 1; - } - targetCompilerFilter = compilerFilter; - } - if (compilationReasonGiven) { - int reason = -1; - for (int i = 0; i < PackageManagerServiceCompilerMapping.REASON_STRINGS.length; i++) { - if (PackageManagerServiceCompilerMapping.REASON_STRINGS[i].equals( - compilationReason)) { - reason = i; - break; - } - } - if (reason == -1) { - pw.println("Error: Unknown compilation reason: " + compilationReason); - return 1; - } - targetCompilerFilter = - PackageManagerServiceCompilerMapping.getCompilerFilterForReason(reason); - } - - - List<String> packageNames = null; - if (allPackages) { - packageNames = mInterface.getAllPackages(); - // Compiling the system server is only supported from odrefresh, so skip it. - packageNames.removeIf(packageName -> PLATFORM_PACKAGE_NAME.equals(packageName)); - } else { - String packageName = getNextArg(); - if (packageName == null) { - pw.println("Error: package name not specified"); - return 1; - } - packageNames = Collections.singletonList(packageName); - } - - List<String> failedPackages = new ArrayList<>(); - int index = 0; - for (String packageName : packageNames) { - if (clearProfileData) { - mInterface.clearApplicationProfileData(packageName); - } - - if (allPackages) { - pw.println(++index + "/" + packageNames.size() + ": " + packageName); - pw.flush(); - } - - final boolean result = secondaryDex - ? mInterface.performDexOptSecondary( - packageName, targetCompilerFilter, forceCompilation) - : mInterface.performDexOptMode(packageName, true /* checkProfiles */, - targetCompilerFilter, forceCompilation, true /* bootComplete */, split); - if (!result) { - failedPackages.add(packageName); - } - } - - if (failedPackages.isEmpty()) { - pw.println("Success"); - return 0; - } else if (failedPackages.size() == 1) { - pw.println("Failure: package " + failedPackages.get(0) + " could not be compiled"); - return 1; - } else { - pw.print("Failure: the following packages could not be compiled: "); - boolean is_first = true; - for (String packageName : failedPackages) { - if (is_first) { - is_first = false; - } else { - pw.print(", "); - } - pw.print(packageName); - } - pw.println(); - return 1; - } - } - - private int runreconcileSecondaryDexFiles() - throws RemoteException, LegacyDexoptDisabledException { - String packageName = getNextArg(); - mPm.legacyReconcileSecondaryDexFiles(packageName); - return 0; - } - - public int runForceDexOpt() throws RemoteException, LegacyDexoptDisabledException { - mPm.legacyForceDexOpt(getNextArgRequired()); - return 0; - } - - private int runBgDexOpt() throws RemoteException, LegacyDexoptDisabledException { - String opt = getNextOption(); - - if (opt == null) { - List<String> packageNames = new ArrayList<>(); - String arg; - while ((arg = getNextArg()) != null) { - packageNames.add(arg); - } - if (!BackgroundDexOptService.getService().runBackgroundDexoptJob( - packageNames.isEmpty() ? null : packageNames)) { - getOutPrintWriter().println("Failure"); - return -1; - } - } else { - String extraArg = getNextArg(); - if (extraArg != null) { - getErrPrintWriter().println("Invalid argument: " + extraArg); - return -1; - } - - switch (opt) { - case "--cancel": - return cancelBgDexOptJob(); - - case "--disable": - BackgroundDexOptService.getService().setDisableJobSchedulerJobs(true); - break; - - case "--enable": - BackgroundDexOptService.getService().setDisableJobSchedulerJobs(false); - break; - - default: - getErrPrintWriter().println("Unknown option: " + opt); - return -1; - } - } - - getOutPrintWriter().println("Success"); - return 0; - } - - private int cancelBgDexOptJob() throws RemoteException, LegacyDexoptDisabledException { - BackgroundDexOptService.getService().cancelBackgroundDexoptJob(); - getOutPrintWriter().println("Success"); - return 0; - } - - private int runDeleteDexOpt() throws RemoteException { - PrintWriter pw = getOutPrintWriter(); - String packageName = getNextArg(); - if (TextUtils.isEmpty(packageName)) { - pw.println("Error: no package name"); - return 1; - } - long freedBytes = mPm.deleteOatArtifactsOfPackage(packageName); - if (freedBytes < 0) { - pw.println("Error: delete failed"); - return 1; - } - pw.println("Success: freed " + freedBytes + " bytes"); - Slog.i(TAG, "delete-dexopt " + packageName + " ,freed " + freedBytes + " bytes"); - return 0; - } - - private int runDumpProfiles() throws RemoteException, LegacyDexoptDisabledException { - final PrintWriter pw = getOutPrintWriter(); - boolean dumpClassesAndMethods = false; - - String opt; - while ((opt = getNextOption()) != null) { - switch (opt) { - case "--dump-classes-and-methods": - dumpClassesAndMethods = true; - break; - default: - pw.println("Error: Unknown option: " + opt); - return 1; - } - } - - String packageName = getNextArg(); - mPm.legacyDumpProfiles(packageName, dumpClassesAndMethods); - return 0; - } - - private int runSnapshotProfile() throws RemoteException { - PrintWriter pw = getOutPrintWriter(); - - // Parse the arguments - final String packageName = getNextArg(); - final boolean isBootImage = "android".equals(packageName); - - String codePath = null; - String opt; - while ((opt = getNextArg()) != null) { - switch (opt) { - case "--code-path": - if (isBootImage) { - pw.write("--code-path cannot be used for the boot image."); - return -1; - } - codePath = getNextArg(); - break; - default: - pw.write("Unknown arg: " + opt); - return -1; - } - } - - // If no code path was explicitly requested, select the base code path. - String baseCodePath = null; - if (!isBootImage) { - PackageInfo packageInfo = mInterface.getPackageInfo(packageName, /* flags */ 0, - /* userId */0); - if (packageInfo == null) { - pw.write("Package not found " + packageName); - return -1; - } - baseCodePath = packageInfo.applicationInfo.getBaseCodePath(); - if (codePath == null) { - codePath = baseCodePath; - } - } - - // Create the profile snapshot. - final SnapshotRuntimeProfileCallback callback = new SnapshotRuntimeProfileCallback(); - // The calling package is needed to debug permission access. - final String callingPackage = (Binder.getCallingUid() == Process.ROOT_UID) - ? "root" : "com.android.shell"; - final int profileType = isBootImage - ? ArtManager.PROFILE_BOOT_IMAGE : ArtManager.PROFILE_APPS; - if (!mInterface.getArtManager().isRuntimeProfilingEnabled(profileType, callingPackage)) { - pw.println("Error: Runtime profiling is not enabled"); - return -1; - } - mInterface.getArtManager().snapshotRuntimeProfile(profileType, packageName, - codePath, callback, callingPackage); - if (!callback.waitTillDone()) { - pw.println("Error: callback not called"); - return callback.mErrCode; - } - - // Copy the snapshot profile to the output profile file. - try (InputStream inStream = new AutoCloseInputStream(callback.mProfileReadFd)) { - final String outputFileSuffix = isBootImage || Objects.equals(baseCodePath, codePath) - ? "" : ("-" + new File(codePath).getName()); - final String outputProfilePath = - ART_PROFILE_SNAPSHOT_DEBUG_LOCATION + packageName + outputFileSuffix + ".prof"; - try (OutputStream outStream = new FileOutputStream(outputProfilePath)) { - Streams.copy(inStream, outStream); - } - // Give read permissions to the other group. - Os.chmod(outputProfilePath, /*mode*/ DEFAULT_FILE_ACCESS_MODE); - } catch (IOException | ErrnoException e) { - pw.println("Error when reading the profile fd: " + e.getMessage()); - e.printStackTrace(pw); - return -1; - } - return 0; - } - private ArrayList<String> getRemainingArgs() { ArrayList<String> args = new ArrayList<>(); String arg; @@ -5212,11 +4826,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" get-domain-verification-agent"); pw.println(" Displays the component name of the domain verification agent on device."); pw.println(""); - if (DexOptHelper.useArtService()) { - printArtServiceHelp(); - } else { - printLegacyDexoptHelp(); - } + printArtServiceHelp(); pw.println(""); mDomainVerificationShell.printHelp(pw); pw.println(""); @@ -5235,75 +4845,6 @@ class PackageManagerShellCommand extends ShellCommand { ipw.decreaseIndent(); } - private void printLegacyDexoptHelp() { - final PrintWriter pw = getOutPrintWriter(); - pw.println(" compile [-m MODE | -r REASON] [-f] [-c] [--split SPLIT_NAME]"); - pw.println(" [--reset] [--check-prof (true | false)] (-a | TARGET-PACKAGE)"); - pw.println(" Trigger compilation of TARGET-PACKAGE or all packages if \"-a\". Options are:"); - pw.println(" -a: compile all packages"); - pw.println(" -c: clear profile data before compiling"); - pw.println(" -f: force compilation even if not needed"); - pw.println(" -m: select compilation mode"); - pw.println(" MODE is one of the dex2oat compiler filters:"); - pw.println(" verify"); - pw.println(" speed-profile"); - pw.println(" speed"); - pw.println(" -r: select compilation reason"); - pw.println(" REASON is one of:"); - for (int i = 0; i < PackageManagerServiceCompilerMapping.REASON_STRINGS.length; i++) { - pw.println(" " + PackageManagerServiceCompilerMapping.REASON_STRINGS[i]); - } - pw.println(" --reset: restore package to its post-install state"); - pw.println(" --check-prof (true | false): ignored - this is always true"); - pw.println(" --secondary-dex: compile app secondary dex files"); - pw.println(" --split SPLIT: compile only the given split name"); - pw.println(""); - pw.println(" force-dex-opt PACKAGE"); - pw.println(" Force immediate execution of dex opt for the given PACKAGE."); - pw.println(""); - pw.println(" delete-dexopt PACKAGE"); - pw.println(" Delete dex optimization results for the given PACKAGE."); - pw.println(""); - pw.println(" bg-dexopt-job [PACKAGE... | --cancel | --disable | --enable]"); - pw.println(" Controls the background job that optimizes dex files:"); - pw.println(" Without flags, run background optimization immediately on the given"); - pw.println(" PACKAGEs, or all packages if none is specified, and wait until the job"); - pw.println(" finishes. Note that the command only runs the background optimizer logic."); - pw.println(" It will run even if the device is not in the idle maintenance mode. If a"); - pw.println(" job is already running (including one started automatically by the"); - pw.println(" system) it will wait for it to finish before starting. A background job"); - pw.println(" will not be started automatically while one started this way is running."); - pw.println(" --cancel: Cancels any currently running background optimization job"); - pw.println(" immediately. This cancels jobs started either automatically by the"); - pw.println(" system or through this command. Note that cancelling a currently"); - pw.println(" running bg-dexopt-job command requires running this command from a"); - pw.println(" separate adb shell."); - pw.println(" --disable: Disables background jobs from being started by the job"); - pw.println(" scheduler. Does not affect bg-dexopt-job invocations from the shell."); - pw.println(" Does not imply --cancel. This state will be lost when the"); - pw.println(" system_server process exits."); - pw.println(" --enable: Enables background jobs to be started by the job scheduler"); - pw.println(" again, if previously disabled by --disable."); - pw.println(" cancel-bg-dexopt-job"); - pw.println(" Same as bg-dexopt-job --cancel."); - pw.println(""); - pw.println(" reconcile-secondary-dex-files TARGET-PACKAGE"); - pw.println(" Reconciles the package secondary dex files with the generated oat files."); - pw.println(""); - pw.println(" dump-profiles [--dump-classes-and-methods] TARGET-PACKAGE"); - pw.println(" Dumps method/class profile files to"); - pw.println(" " + ART_PROFILE_SNAPSHOT_DEBUG_LOCATION - + "TARGET-PACKAGE-primary.prof.txt."); - pw.println(" --dump-classes-and-methods: passed along to the profman binary to"); - pw.println(" switch to the format used by 'profman --create-profile-from'."); - pw.println(""); - pw.println(" snapshot-profile TARGET-PACKAGE [--code-path path]"); - pw.println(" Take a snapshot of the package profiles to"); - pw.println(" " + ART_PROFILE_SNAPSHOT_DEBUG_LOCATION - + "TARGET-PACKAGE[-code-path].prof"); - pw.println(" If TARGET-PACKAGE=android it will take a snapshot of the boot image"); - } - private static class LocalIntentReceiver { private final LinkedBlockingQueue<Intent> mResult = new LinkedBlockingQueue<>(); diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 12eb88e518e6..b44042c75e80 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -16,6 +16,7 @@ package com.android.server.pm; +import static android.app.admin.flags.Flags.crossUserSuspensionEnabled; import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; @@ -1240,6 +1241,10 @@ public class PackageSetting extends SettingBase implements PackageStateInternal for (int j = 0; j < state.getSuspendParams().size(); j++) { proto.write(PackageProto.UserInfoProto.SUSPENDING_PACKAGE, state.getSuspendParams().keyAt(j).packageName); + if (crossUserSuspensionEnabled()) { + proto.write(PackageProto.UserInfoProto.SUSPENDING_USER, + state.getSuspendParams().keyAt(j).userId); + } } } proto.write(PackageProto.UserInfoProto.IS_STOPPED, state.isStopped()); diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index 70352be01096..3a0f7fb4b432 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -23,7 +23,6 @@ import static android.os.storage.StorageManager.FLAG_STORAGE_CE; import static android.os.storage.StorageManager.FLAG_STORAGE_DE; import static android.os.storage.StorageManager.FLAG_STORAGE_EXTERNAL; -import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets; import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL; import static com.android.server.pm.PackageManagerService.DEBUG_REMOVE; import static com.android.server.pm.PackageManagerService.RANDOM_DIR_PREFIX; @@ -49,7 +48,6 @@ import com.android.internal.pm.parsing.pkg.AndroidPackageLegacyUtils; import com.android.internal.pm.parsing.pkg.PackageImpl; import com.android.internal.pm.pkg.component.ParsedInstrumentation; import com.android.internal.util.ArrayUtils; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.parsing.PackageCacher; import com.android.server.pm.permission.PermissionManagerServiceInternal; import com.android.server.pm.pkg.AndroidPackage; @@ -263,11 +261,6 @@ final class RemovePackageHelper { // Step 1: always destroy app profiles. mAppDataHelper.destroyAppProfilesLIF(packageName); - // Everything else is preserved if the DELETE_KEEP_DATA flag is on - if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) { - return; - } - final AndroidPackage pkg; final SharedUserSetting sus; synchronized (mPm.mLock) { @@ -284,9 +277,20 @@ final class RemovePackageHelper { resolvedPkg = PackageImpl.buildFakeForDeletion(packageName, ps.getVolumeUuid()); } + int appDataDeletionFlags = FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL; + // Personal data is preserved if the DELETE_KEEP_DATA flag is on + if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) { + if ((flags & PackageManager.DELETE_ARCHIVE) != 0) { + mAppDataHelper.clearAppDataLIF(resolvedPkg, userId, + appDataDeletionFlags | Installer.FLAG_CLEAR_CACHE_ONLY); + mAppDataHelper.clearAppDataLIF(resolvedPkg, userId, + appDataDeletionFlags | Installer.FLAG_CLEAR_CODE_CACHE_ONLY); + } + return; + } + // Step 2: destroy app data. - mAppDataHelper.destroyAppDataLIF(resolvedPkg, userId, - FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL); + mAppDataHelper.destroyAppDataLIF(resolvedPkg, userId, appDataDeletionFlags); if (userId != UserHandle.USER_ALL) { ps.setCeDataInode(-1, userId); ps.setDeDataInode(-1, userId); @@ -511,32 +515,9 @@ final class RemovePackageHelper { } removeCodePathLI(codeFile); - removeDexFilesLI(allCodePaths, instructionSets); - } - @GuardedBy("mPm.mInstallLock") - private void removeDexFilesLI(@NonNull List<String> allCodePaths, - @Nullable String[] instructionSets) { - if (!allCodePaths.isEmpty()) { - if (instructionSets == null) { - throw new IllegalStateException("instructionSet == null"); - } - // TODO(b/265813358): ART Service currently doesn't support deleting optimized artifacts - // relative to an arbitrary APK path. Skip this and rely on its file GC instead. - if (!DexOptHelper.useArtService()) { - String[] dexCodeInstructionSets = getDexCodeInstructionSets(instructionSets); - for (String codePath : allCodePaths) { - for (String dexCodeInstructionSet : dexCodeInstructionSets) { - try { - mPm.mInstaller.rmdex(codePath, dexCodeInstructionSet); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } catch (Installer.InstallerException ignored) { - } - } - } - } - } + // TODO(b/265813358): ART Service currently doesn't support deleting optimized artifacts + // relative to an arbitrary APK path. Skip this and rely on its file GC instead. } void cleanUpForMoveInstall(String volumeUuid, String packageName, String fromCodePath) { diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index e35a169cdd60..f5ed8d4af45b 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -16,6 +16,7 @@ package com.android.server.pm; +import static android.app.admin.flags.Flags.crossUserSuspensionEnabled; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED; import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED; @@ -342,6 +343,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile private static final String ATTR_DISTRACTION_FLAGS = "distraction_flags"; private static final String ATTR_SUSPENDED = "suspended"; private static final String ATTR_SUSPENDING_PACKAGE = "suspending-package"; + private static final String ATTR_SUSPENDING_USER = "suspending-user"; private static final String ATTR_OPTIONAL = "optional"; /** @@ -2051,7 +2053,20 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile Slog.wtf(TAG, "No suspendingPackage found inside tag " + TAG_SUSPEND_PARAMS); return null; } - final int suspendingUserId = userId; + int suspendingUserId; + if (crossUserSuspensionEnabled()) { + suspendingUserId = parser.getAttributeInt( + null, ATTR_SUSPENDING_USER, UserHandle.USER_NULL); + if (suspendingUserId == UserHandle.USER_NULL) { + suspendingUserId = switch (suspendingPackage) { + case "root", "com.android.shell", PLATFORM_PACKAGE_NAME + -> UserHandle.USER_SYSTEM; + default -> userId; + }; + } + } else { + suspendingUserId = userId; + } return Map.entry( UserPackage.of(suspendingUserId, suspendingPackage), SuspendParams.restoreFromXml(parser)); @@ -2418,6 +2433,10 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile serializer.startTag(null, TAG_SUSPEND_PARAMS); serializer.attribute(null, ATTR_SUSPENDING_PACKAGE, suspendingPackage.packageName); + if (crossUserSuspensionEnabled()) { + serializer.attributeInt(null, ATTR_SUSPENDING_USER, + suspendingPackage.userId); + } final SuspendParams params = ustate.getSuspendParams().valueAt(i); if (params != null) { diff --git a/services/core/java/com/android/server/pm/dex/ArtManagerService.java b/services/core/java/com/android/server/pm/dex/ArtManagerService.java index ae47aa823245..e49dc8250bc7 100644 --- a/services/core/java/com/android/server/pm/dex/ArtManagerService.java +++ b/services/core/java/com/android/server/pm/dex/ArtManagerService.java @@ -18,7 +18,6 @@ package com.android.server.pm.dex; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -28,7 +27,6 @@ import android.content.pm.PackageManager; import android.content.pm.dex.ArtManager; import android.content.pm.dex.ArtManager.ProfileType; import android.content.pm.dex.ArtManagerInternal; -import android.content.pm.dex.DexMetadataHelper; import android.content.pm.dex.ISnapshotRuntimeProfileCallback; import android.content.pm.dex.PackageOptimizationInfo; import android.os.Binder; @@ -39,8 +37,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; -import android.os.UserHandle; -import android.system.Os; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; @@ -53,22 +49,17 @@ import com.android.server.LocalServices; import com.android.server.art.ArtManagerLocal; import com.android.server.pm.DexOptHelper; import com.android.server.pm.Installer; -import com.android.server.pm.Installer.InstallerException; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.PackageManagerLocal; import com.android.server.pm.PackageManagerService; import com.android.server.pm.PackageManagerServiceCompilerMapping; import com.android.server.pm.PackageManagerServiceUtils; -import com.android.server.pm.parsing.PackageInfoUtils; import com.android.server.pm.pkg.AndroidPackage; -import com.android.server.pm.pkg.PackageStateInternal; import dalvik.system.DexFile; import dalvik.system.VMRuntime; import libcore.io.IoUtils; -import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; @@ -259,91 +250,27 @@ public class ArtManagerService extends android.content.pm.dex.IArtManager.Stub { } // All good, create the profile snapshot. - if (DexOptHelper.useArtService()) { - ParcelFileDescriptor fd; - - try (PackageManagerLocal.FilteredSnapshot snapshot = - PackageManagerServiceUtils.getPackageManagerLocal() - .withFilteredSnapshot()) { - fd = DexOptHelper.getArtManagerLocal().snapshotAppProfile( - snapshot, packageName, splitName); - } catch (IllegalArgumentException e) { - // ArtManagerLocal.snapshotAppProfile couldn't find the package or split. Since - // we've checked them above this can only happen due to race, i.e. the package got - // removed. So let's report it as SNAPSHOT_FAILED_PACKAGE_NOT_FOUND even if it was - // for the split. - // TODO(mast): Reuse the same snapshot to avoid this race. - postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_PACKAGE_NOT_FOUND); - return; - } catch (IllegalStateException | ArtManagerLocal.SnapshotProfileException e) { - postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR); - return; - } - - postSuccess(packageName, fd, callback); - } else { - int appId = UserHandle.getAppId(info.applicationInfo.uid); - if (appId < 0) { - postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR); - Slog.wtf(TAG, "AppId is -1 for package: " + packageName); - return; - } - - try { - createProfileSnapshot(packageName, ArtManager.getProfileName(splitName), codePath, - appId, callback); - // Destroy the snapshot, we no longer need it. - destroyProfileSnapshot(packageName, ArtManager.getProfileName(splitName)); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } - } - } - - private void createProfileSnapshot(String packageName, String profileName, String classpath, - int appId, ISnapshotRuntimeProfileCallback callback) - throws LegacyDexoptDisabledException { - // Ask the installer to snapshot the profile. - try { - if (!mInstaller.createProfileSnapshot(appId, packageName, profileName, classpath)) { - postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR); - return; - } - } catch (InstallerException e) { - postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR); + ParcelFileDescriptor fd; + + try (PackageManagerLocal.FilteredSnapshot snapshot = + PackageManagerServiceUtils.getPackageManagerLocal() + .withFilteredSnapshot()) { + fd = DexOptHelper.getArtManagerLocal().snapshotAppProfile( + snapshot, packageName, splitName); + } catch (IllegalArgumentException e) { + // ArtManagerLocal.snapshotAppProfile couldn't find the package or split. Since + // we've checked them above this can only happen due to race, i.e. the package got + // removed. So let's report it as SNAPSHOT_FAILED_PACKAGE_NOT_FOUND even if it was + // for the split. + // TODO(mast): Reuse the same snapshot to avoid this race. + postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_PACKAGE_NOT_FOUND); return; - } - - // Open the snapshot and invoke the callback. - File snapshotProfile = ArtManager.getProfileSnapshotFileForName(packageName, profileName); - - ParcelFileDescriptor fd = null; - try { - fd = ParcelFileDescriptor.open(snapshotProfile, ParcelFileDescriptor.MODE_READ_ONLY); - if (fd == null || !fd.getFileDescriptor().valid()) { - postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR); - } else { - postSuccess(packageName, fd, callback); - } - } catch (FileNotFoundException e) { - Slog.w(TAG, "Could not open snapshot profile for " + packageName + ":" - + snapshotProfile, e); + } catch (IllegalStateException | ArtManagerLocal.SnapshotProfileException e) { postError(callback, packageName, ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR); - } - } - - private void destroyProfileSnapshot(String packageName, String profileName) - throws LegacyDexoptDisabledException { - if (DEBUG) { - Slog.d(TAG, "Destroying profile snapshot for" + packageName + ":" + profileName); + return; } - try { - mInstaller.destroyProfileSnapshot(packageName, profileName); - } catch (InstallerException e) { - Slog.e(TAG, "Failed to destroy profile snapshot for " + packageName + ":" + profileName, - e); - } + postSuccess(packageName, fd, callback); } @Override @@ -368,42 +295,19 @@ public class ArtManagerService extends android.content.pm.dex.IArtManager.Stub { } private void snapshotBootImageProfile(ISnapshotRuntimeProfileCallback callback) { - if (DexOptHelper.useArtService()) { - ParcelFileDescriptor fd; - - try (PackageManagerLocal.FilteredSnapshot snapshot = - PackageManagerServiceUtils.getPackageManagerLocal() - .withFilteredSnapshot()) { - fd = DexOptHelper.getArtManagerLocal().snapshotBootImageProfile(snapshot); - } catch (IllegalStateException | ArtManagerLocal.SnapshotProfileException e) { - postError(callback, BOOT_IMAGE_ANDROID_PACKAGE, - ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR); - return; - } - - postSuccess(BOOT_IMAGE_ANDROID_PACKAGE, fd, callback); - } else { - // Combine the profiles for boot classpath and system server classpath. - // This avoids having yet another type of profiles and simplifies the processing. - String classpath = String.join( - ":", Os.getenv("BOOTCLASSPATH"), Os.getenv("SYSTEMSERVERCLASSPATH")); - - final String standaloneSystemServerJars = Os.getenv("STANDALONE_SYSTEMSERVER_JARS"); - if (standaloneSystemServerJars != null) { - classpath = String.join(":", classpath, standaloneSystemServerJars); - } - - try { - // Create the snapshot. - createProfileSnapshot(BOOT_IMAGE_ANDROID_PACKAGE, BOOT_IMAGE_PROFILE_NAME, - classpath, - /*appId*/ -1, callback); - // Destroy the snapshot, we no longer need it. - destroyProfileSnapshot(BOOT_IMAGE_ANDROID_PACKAGE, BOOT_IMAGE_PROFILE_NAME); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); - } + ParcelFileDescriptor fd; + + try (PackageManagerLocal.FilteredSnapshot snapshot = + PackageManagerServiceUtils.getPackageManagerLocal() + .withFilteredSnapshot()) { + fd = DexOptHelper.getArtManagerLocal().snapshotBootImageProfile(snapshot); + } catch (IllegalStateException | ArtManagerLocal.SnapshotProfileException e) { + postError(callback, BOOT_IMAGE_ANDROID_PACKAGE, + ArtManager.SNAPSHOT_FAILED_INTERNAL_ERROR); + return; } + + postSuccess(BOOT_IMAGE_ANDROID_PACKAGE, fd, callback); } /** @@ -451,117 +355,6 @@ public class ArtManagerService extends android.content.pm.dex.IArtManager.Stub { }); } - /** - * Prepare the application profiles. - * For all code paths: - * - create the current primary profile to save time at app startup time. - * - copy the profiles from the associated dex metadata file to the reference profile. - */ - public void prepareAppProfiles(AndroidPackage pkg, @UserIdInt int user, - boolean updateReferenceProfileContent) throws LegacyDexoptDisabledException { - final int appId = UserHandle.getAppId(pkg.getUid()); - if (user < 0) { - Slog.wtf(TAG, "Invalid user id: " + user); - return; - } - if (appId < 0) { - Slog.wtf(TAG, "Invalid app id: " + appId); - return; - } - try { - ArrayMap<String, String> codePathsProfileNames = getPackageProfileNames(pkg); - for (int i = codePathsProfileNames.size() - 1; i >= 0; i--) { - String codePath = codePathsProfileNames.keyAt(i); - String profileName = codePathsProfileNames.valueAt(i); - String dexMetadataPath = null; - // Passing the dex metadata file to the prepare method will update the reference - // profile content. As such, we look for the dex metadata file only if we need to - // perform an update. - if (updateReferenceProfileContent) { - File dexMetadata = DexMetadataHelper.findDexMetadataForFile(new File(codePath)); - dexMetadataPath = dexMetadata == null ? null : dexMetadata.getAbsolutePath(); - } - synchronized (mInstaller) { - boolean result = mInstaller.prepareAppProfile(pkg.getPackageName(), user, appId, - profileName, codePath, dexMetadataPath); - if (!result) { - Slog.e(TAG, "Failed to prepare profile for " + - pkg.getPackageName() + ":" + codePath); - } - } - } - } catch (InstallerException e) { - Slog.e(TAG, "Failed to prepare profile for " + pkg.getPackageName(), e); - } - } - - /** - * Prepares the app profiles for a set of users. {@see ArtManagerService#prepareAppProfiles}. - */ - public void prepareAppProfiles(AndroidPackage pkg, int[] user, - boolean updateReferenceProfileContent) throws LegacyDexoptDisabledException { - for (int i = 0; i < user.length; i++) { - prepareAppProfiles(pkg, user[i], updateReferenceProfileContent); - } - } - - /** - * Clear the profiles for the given package. - */ - public void clearAppProfiles(AndroidPackage pkg) throws LegacyDexoptDisabledException { - try { - ArrayMap<String, String> packageProfileNames = getPackageProfileNames(pkg); - for (int i = packageProfileNames.size() - 1; i >= 0; i--) { - String profileName = packageProfileNames.valueAt(i); - mInstaller.clearAppProfiles(pkg.getPackageName(), profileName); - } - } catch (InstallerException e) { - Slog.w(TAG, String.valueOf(e)); - } - } - - /** - * Dumps the profiles for the given package. - */ - public void dumpProfiles(AndroidPackage pkg, boolean dumpClassesAndMethods) - throws LegacyDexoptDisabledException { - final int sharedGid = UserHandle.getSharedAppGid(pkg.getUid()); - try { - ArrayMap<String, String> packageProfileNames = getPackageProfileNames(pkg); - for (int i = packageProfileNames.size() - 1; i >= 0; i--) { - String codePath = packageProfileNames.keyAt(i); - String profileName = packageProfileNames.valueAt(i); - mInstaller.dumpProfiles(sharedGid, pkg.getPackageName(), profileName, codePath, - dumpClassesAndMethods); - } - } catch (InstallerException e) { - Slog.w(TAG, "Failed to dump profiles", e); - } - } - - /** - * Build the profiles names for all the package code paths (excluding resource only paths). - * Return the map [code path -> profile name]. - */ - private ArrayMap<String, String> getPackageProfileNames(AndroidPackage pkg) { - ArrayMap<String, String> result = new ArrayMap<>(); - if (pkg.isDeclaredHavingCode()) { - result.put(pkg.getBaseApkPath(), ArtManager.getProfileName(null)); - } - - String[] splitCodePaths = pkg.getSplitCodePaths(); - int[] splitFlags = pkg.getSplitFlags(); - String[] splitNames = pkg.getSplitNames(); - if (!ArrayUtils.isEmpty(splitCodePaths)) { - for (int i = 0; i < splitCodePaths.length; i++) { - if ((splitFlags[i] & ApplicationInfo.FLAG_HAS_CODE) != 0) { - result.put(splitCodePaths[i], ArtManager.getProfileName(splitNames[i])); - } - } - } - return result; - } - // Constants used for logging compilation filter to TRON. // DO NOT CHANGE existing values. // @@ -792,6 +585,7 @@ public class ArtManagerService extends android.content.pm.dex.IArtManager.Stub { String packageName, String activityName, long version) { // For example: /data/misc/iorapd/com.google.android.GoogleCamera/ // 60092239/com.android.camera.CameraLauncher/compiled_traces/compiled_trace.pb + // TODO(b/258223472): Clean up iorap code. Path tracePath = Paths.get(IORAP_DIR, packageName, Long.toString(version), diff --git a/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java b/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java index 57f4a5ddb2bd..a24a2318d423 100644 --- a/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java +++ b/services/core/java/com/android/server/pm/dex/ArtStatsLogUtils.java @@ -22,13 +22,11 @@ import static com.android.internal.art.ArtStatsLog.ART_DATUM_REPORTED__COMPILATI import static com.android.internal.art.ArtStatsLog.ART_DATUM_REPORTED__COMPILE_FILTER__ART_COMPILATION_FILTER_FAKE_RUN_FROM_APK_FALLBACK; import static com.android.internal.art.ArtStatsLog.ART_DATUM_REPORTED__COMPILE_FILTER__ART_COMPILATION_FILTER_FAKE_RUN_FROM_VDEX_FALLBACK; -import android.app.job.JobParameters; import android.os.SystemClock; import android.util.Slog; import android.util.jar.StrictJarFile; import com.android.internal.art.ArtStatsLog; -import com.android.server.pm.BackgroundDexOptService; import com.android.server.pm.PackageManagerService; import java.io.IOException; @@ -303,42 +301,4 @@ public class ArtStatsLogUtils { ArtStatsLog.ART_DATUM_REPORTED__UFFD_SUPPORT__ART_UFFD_SUPPORT_UNKNOWN); } } - - private static final Map<Integer, Integer> STATUS_MAP = - Map.of(BackgroundDexOptService.STATUS_UNSPECIFIED, - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_UNKNOWN, - BackgroundDexOptService.STATUS_OK, - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_JOB_FINISHED, - BackgroundDexOptService.STATUS_ABORT_BY_CANCELLATION, - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BY_CANCELLATION, - BackgroundDexOptService.STATUS_ABORT_NO_SPACE_LEFT, - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_NO_SPACE_LEFT, - BackgroundDexOptService.STATUS_ABORT_THERMAL, - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_THERMAL, - BackgroundDexOptService.STATUS_ABORT_BATTERY, - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BATTERY, - BackgroundDexOptService.STATUS_DEX_OPT_FAILED, - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_JOB_FINISHED, - BackgroundDexOptService.STATUS_FATAL_ERROR, - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_FATAL_ERROR); - - /** Helper class to write background dexopt job stats to statsd. */ - public static class BackgroundDexoptJobStatsLogger { - /** Writes background dexopt job stats to statsd. */ - public void write(@BackgroundDexOptService.Status int status, - @JobParameters.StopReason int cancellationReason, - long durationMs) { - ArtStatsLog.write( - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED, - STATUS_MAP.getOrDefault(status, - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_UNKNOWN), - cancellationReason, - durationMs, - 0, // deprecated, used to be durationIncludingSleepMs - 0, // optimizedPackagesCount - 0, // packagesDependingOnBootClasspathCount - 0, // totalPackagesCount - ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__PASS__PASS_UNKNOWN); - } - } } diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java index 78c13f854fe4..e93d3206a4f1 100644 --- a/services/core/java/com/android/server/pm/dex/DexManager.java +++ b/services/core/java/com/android/server/pm/dex/DexManager.java @@ -17,7 +17,6 @@ package com.android.server.pm.dex; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; -import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo; import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo; import static java.util.function.Function.identity; @@ -31,12 +30,9 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackagePartitions; import android.os.BatteryManager; -import android.os.FileUtils; import android.os.PowerManager; -import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; -import android.os.storage.StorageManager; import android.util.Log; import android.util.Slog; import android.util.jar.StrictJarFile; @@ -44,8 +40,6 @@ import android.util.jar.StrictJarFile; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.pm.Installer; -import com.android.server.pm.Installer.InstallerException; -import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.PackageDexOptimizer; import com.android.server.pm.PackageManagerService; import com.android.server.pm.PackageManagerServiceUtils; @@ -54,8 +48,6 @@ import dalvik.system.VMRuntime; import java.io.File; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -496,60 +488,6 @@ public class DexManager { } /** - * Perform dexopt on with the given {@code options} on the secondary dex files. - * @return true if all secondary dex files were processed successfully (compiled or skipped - * because they don't need to be compiled).. - */ - public boolean dexoptSecondaryDex(DexoptOptions options) throws LegacyDexoptDisabledException { - if (isPlatformPackage(options.getPackageName())) { - // We could easily redirect to #dexoptSystemServer in this case. But there should be - // no-one calling this method directly for system server. - // As such we prefer to abort in this case. - Slog.wtf(TAG, "System server jars should be optimized with dexoptSystemServer"); - return false; - } - - PackageDexOptimizer pdo = getPackageDexOptimizer(options); - String packageName = options.getPackageName(); - PackageUseInfo useInfo = getPackageUseInfoOrDefault(packageName); - if (useInfo.getDexUseInfoMap().isEmpty()) { - if (DEBUG) { - Slog.d(TAG, "No secondary dex use for package:" + packageName); - } - // Nothing to compile, return true. - return true; - } - boolean success = true; - for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) { - String dexPath = entry.getKey(); - DexUseInfo dexUseInfo = entry.getValue(); - - PackageInfo pkg; - try { - pkg = getPackageManager().getPackageInfo(packageName, /*flags*/0, - dexUseInfo.getOwnerUserId()); - } catch (RemoteException e) { - throw new AssertionError(e); - } - // It may be that the package gets uninstalled while we try to compile its - // secondary dex files. If that's the case, just ignore. - // Note that we don't break the entire loop because the package might still be - // installed for other users. - if (pkg == null) { - Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName - + " for user " + dexUseInfo.getOwnerUserId()); - mPackageDexUsage.removeUserPackage(packageName, dexUseInfo.getOwnerUserId()); - continue; - } - - int result = pdo.dexOptSecondaryDexPath(pkg.applicationInfo, dexPath, - dexUseInfo, options); - success = success && (result != PackageDexOptimizer.DEX_OPT_FAILED); - } - return success; - } - - /** * Select the dex optimizer based on the force parameter. * Forced compilation is done through ForcedUpdatePackageDexOptimizer which will adjust * the necessary dexopt flags to make sure that compilation is not skipped. This avoid @@ -564,101 +502,6 @@ public class DexManager { } /** - * Reconcile the information we have about the secondary dex files belonging to - * {@code packagName} and the actual dex files. For all dex files that were - * deleted, update the internal records and delete any generated oat files. - */ - public void reconcileSecondaryDexFiles(String packageName) - throws LegacyDexoptDisabledException { - PackageUseInfo useInfo = getPackageUseInfoOrDefault(packageName); - if (useInfo.getDexUseInfoMap().isEmpty()) { - if (DEBUG) { - Slog.d(TAG, "No secondary dex use for package:" + packageName); - } - // Nothing to reconcile. - return; - } - - boolean updated = false; - for (Map.Entry<String, DexUseInfo> entry : useInfo.getDexUseInfoMap().entrySet()) { - String dexPath = entry.getKey(); - DexUseInfo dexUseInfo = entry.getValue(); - PackageInfo pkg = null; - try { - // Note that we look for the package in the PackageManager just to be able - // to get back the real app uid and its storage kind. These are only used - // to perform extra validation in installd. - // TODO(calin): maybe a bit overkill. - pkg = getPackageManager().getPackageInfo(packageName, /*flags*/0, - dexUseInfo.getOwnerUserId()); - } catch (RemoteException ignore) { - // Can't happen, DexManager is local. - } - if (pkg == null) { - // It may be that the package was uninstalled while we process the secondary - // dex files. - Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName - + " for user " + dexUseInfo.getOwnerUserId()); - // Update the usage and continue, another user might still have the package. - updated = mPackageDexUsage.removeUserPackage( - packageName, dexUseInfo.getOwnerUserId()) || updated; - continue; - } - - // Special handle system server files. - // We don't need an installd call because we have permissions to check if the file - // exists. - if (isPlatformPackage(packageName)) { - if (!Files.exists(Paths.get(dexPath))) { - if (DEBUG) { - Slog.w(TAG, "A dex file previously loaded by System Server does not exist " - + " anymore: " + dexPath); - } - updated = mPackageDexUsage.removeUserPackage( - packageName, dexUseInfo.getOwnerUserId()) || updated; - } - continue; - } - - // This is a regular application. - ApplicationInfo info = pkg.applicationInfo; - int flags = 0; - if (info.deviceProtectedDataDir != null && - FileUtils.contains(info.deviceProtectedDataDir, dexPath)) { - flags |= StorageManager.FLAG_STORAGE_DE; - } else if (info.credentialProtectedDataDir!= null && - FileUtils.contains(info.credentialProtectedDataDir, dexPath)) { - flags |= StorageManager.FLAG_STORAGE_CE; - } else { - Slog.e(TAG, "Could not infer CE/DE storage for path " + dexPath); - updated = mPackageDexUsage.removeDexFile( - packageName, dexPath, dexUseInfo.getOwnerUserId()) || updated; - continue; - } - - boolean dexStillExists = true; - synchronized(mInstallLock) { - try { - String[] isas = dexUseInfo.getLoaderIsas().toArray(new String[0]); - dexStillExists = mInstaller.reconcileSecondaryDexFile(dexPath, packageName, - info.uid, isas, info.volumeUuid, flags); - } catch (InstallerException e) { - Slog.e(TAG, "Got InstallerException when reconciling dex " + dexPath + - " : " + e.getMessage()); - } - } - if (!dexStillExists) { - updated = mPackageDexUsage.removeDexFile( - packageName, dexPath, dexUseInfo.getOwnerUserId()) || updated; - } - - } - if (updated) { - mPackageDexUsage.maybeWriteAsync(); - } - } - - /** * Return all packages that contain records of secondary dex files. */ public Set<String> getAllPackagesWithSecondaryDexFiles() { @@ -852,33 +695,6 @@ public class DexManager { return isBtmCritical; } - /** - * Deletes all the optimizations files generated by ART. - * This is best effort, and the method will log but not throw errors - * for individual deletes - * - * @param packageInfo the package information. - * @return the number of freed bytes or -1 if there was an error in the process. - */ - public long deleteOptimizedFiles(ArtPackageInfo packageInfo) - throws LegacyDexoptDisabledException { - long freedBytes = 0; - boolean hadErrors = false; - final String packageName = packageInfo.getPackageName(); - for (String codePath : packageInfo.getCodePaths()) { - for (String isa : packageInfo.getInstructionSets()) { - try { - freedBytes += mInstaller.deleteOdex(packageName, codePath, isa, - packageInfo.getOatDir()); - } catch (InstallerException e) { - Log.e(TAG, "Failed deleting oat files for " + codePath, e); - hadErrors = true; - } - } - } - return hadErrors ? -1 : freedBytes; - } - public static class RegisterDexModuleResult { public RegisterDexModuleResult() { this(false, null); diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 9e31748385c5..76bf8fd45a43 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -530,6 +530,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { // TODO(b/178103325): Track sleep/requested sleep for every display. volatile boolean mRequestedOrSleepingDefaultDisplay; + /** + * This is used to check whether to invoke {@link #updateScreenOffSleepToken} when screen is + * turned off. E.g. if it is false when screen is turned off and the display is swapping, it + * is expected that the screen will be on in a short time. Then it is unnecessary to acquire + * screen-off-sleep-token, so it can avoid intermediate visibility or lifecycle changes. + */ + volatile boolean mIsGoingToSleepDefaultDisplay; + volatile boolean mRecentsVisible; volatile boolean mNavBarVirtualKeyHapticFeedbackEnabled = true; volatile boolean mPictureInPictureVisible; @@ -1905,6 +1913,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { accessibilityManager.performSystemAction( AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS); } + dismissKeyboardShortcutsMenu(); } private void toggleNotificationPanel() { @@ -3478,13 +3487,6 @@ public class PhoneWindowManager implements WindowManagerPolicy { return true; } break; - case KeyEvent.KEYCODE_T: - if (firstDown && event.isMetaPressed()) { - toggleTaskbar(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_TASKBAR); - return true; - } - break; case KeyEvent.KEYCODE_DEL: case KeyEvent.KEYCODE_ESCAPE: if (firstDown && event.isMetaPressed()) { @@ -3506,7 +3508,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) { StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); if (statusbar != null) { - statusbar.enterDesktop(getTargetDisplayIdForKeyEvent(event)); + statusbar.moveFocusedTaskToDesktop(getTargetDisplayIdForKeyEvent(event)); logKeyboardSystemsEvent(event, KeyboardLogEvent.DESKTOP_MODE); return true; } @@ -4735,7 +4737,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (down) { // There may have other embedded activities on the same Task. Try to move the // focus before processing the back event. - mWindowManagerInternal.moveFocusToTopEmbeddedWindowIfNeeded(); + mWindowManagerInternal.moveFocusToAdjacentEmbeddedActivityIfNeeded(); mBackKeyHandled = false; } else { if (!hasLongPressOnBackBehavior()) { @@ -5476,6 +5478,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { } mRequestedOrSleepingDefaultDisplay = true; + mIsGoingToSleepDefaultDisplay = true; + + // In case startedGoingToSleep is called after screenTurnedOff (the source caller is in + // order but the methods run on different threads) and updateScreenOffSleepToken was + // skipped. Then acquire sleep token if screen was off. + if (!mDefaultDisplayPolicy.isScreenOnFully() && !mDefaultDisplayPolicy.isScreenOnEarly() + && com.android.window.flags.Flags.skipSleepingWhenSwitchingDisplay()) { + updateScreenOffSleepToken(true /* acquire */, false /* isSwappingDisplay */); + } if (mKeyguardDelegate != null) { mKeyguardDelegate.onStartedGoingToSleep(pmSleepReason); @@ -5499,6 +5510,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { MetricsLogger.histogram(mContext, "screen_timeout", mLockScreenTimeout / 1000); mRequestedOrSleepingDefaultDisplay = false; + mIsGoingToSleepDefaultDisplay = false; mDefaultDisplayPolicy.setAwake(false); // We must get this work done here because the power manager will drop @@ -5534,7 +5546,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } EventLogTags.writeScreenToggled(1); - + mIsGoingToSleepDefaultDisplay = false; mDefaultDisplayPolicy.setAwake(true); // Since goToSleep performs these functions synchronously, we must @@ -5636,7 +5648,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (DEBUG_WAKEUP) Slog.i(TAG, "Display" + displayId + " turned off..."); if (displayId == DEFAULT_DISPLAY) { - updateScreenOffSleepToken(true, isSwappingDisplay); + if (!isSwappingDisplay || mIsGoingToSleepDefaultDisplay + || !com.android.window.flags.Flags.skipSleepingWhenSwitchingDisplay()) { + updateScreenOffSleepToken(true /* acquire */, isSwappingDisplay); + } mRequestedOrSleepingDefaultDisplay = false; mDefaultDisplayPolicy.screenTurnedOff(); synchronized (mLock) { diff --git a/services/core/java/com/android/server/power/hint/Android.bp b/services/core/java/com/android/server/power/hint/Android.bp new file mode 100644 index 000000000000..8a98de673c3d --- /dev/null +++ b/services/core/java/com/android/server/power/hint/Android.bp @@ -0,0 +1,12 @@ +aconfig_declarations { + name: "power_hint_flags", + package: "com.android.server.power.hint", + srcs: [ + "flags.aconfig", + ], +} + +java_aconfig_library { + name: "power_hint_flags_lib", + aconfig_declarations: "power_hint_flags", +} diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index aa1a41eee220..3f1b1c1e99df 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -17,6 +17,7 @@ package com.android.server.power.hint; import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR; +import static com.android.server.power.hint.Flags.powerhintThreadCleanup; import android.annotation.NonNull; import android.app.ActivityManager; @@ -26,9 +27,12 @@ import android.app.UidObserver; import android.content.Context; import android.hardware.power.WorkDuration; import android.os.Binder; +import android.os.Handler; import android.os.IBinder; import android.os.IHintManager; import android.os.IHintSession; +import android.os.Looper; +import android.os.Message; import android.os.PerformanceHintManager; import android.os.Process; import android.os.RemoteException; @@ -36,6 +40,8 @@ import android.os.SystemProperties; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.IntArray; +import android.util.Slog; import android.util.SparseIntArray; import android.util.StatsEvent; @@ -46,20 +52,31 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.Preconditions; import com.android.server.FgThread; import com.android.server.LocalServices; +import com.android.server.ServiceThread; import com.android.server.SystemService; import com.android.server.utils.Slogf; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; /** An hint service implementation that runs in System Server process. */ public final class HintManagerService extends SystemService { private static final String TAG = "HintManagerService"; private static final boolean DEBUG = false; + + private static final int EVENT_CLEAN_UP_UID = 3; + @VisibleForTesting static final int CLEAN_UP_UID_DELAY_MILLIS = 1000; + + @VisibleForTesting final long mHintSessionPreferredRate; // Multi-level map storing all active AppHintSessions. @@ -73,9 +90,15 @@ public final class HintManagerService extends SystemService { /** Lock to protect HAL handles and listen list. */ private final Object mLock = new Object(); + @GuardedBy("mNonIsolatedTidsLock") + private final Map<Integer, Set<Long>> mNonIsolatedTids; + + private final Object mNonIsolatedTidsLock = new Object(); + @VisibleForTesting final MyUidObserver mUidObserver; private final NativeWrapper mNativeWrapper; + private final CleanUpHandler mCleanUpHandler; private final ActivityManagerInternal mAmInternal; @@ -94,6 +117,13 @@ public final class HintManagerService extends SystemService { HintManagerService(Context context, Injector injector) { super(context); mContext = context; + if (powerhintThreadCleanup()) { + mCleanUpHandler = new CleanUpHandler(createCleanUpThread().getLooper()); + mNonIsolatedTids = new HashMap<>(); + } else { + mCleanUpHandler = null; + mNonIsolatedTids = null; + } mActiveSessions = new ArrayMap<>(); mNativeWrapper = injector.createNativeWrapper(); mNativeWrapper.halInit(); @@ -103,6 +133,13 @@ public final class HintManagerService extends SystemService { LocalServices.getService(ActivityManagerInternal.class)); } + private ServiceThread createCleanUpThread() { + final ServiceThread handlerThread = new ServiceThread(TAG, + Process.THREAD_PRIORITY_LOWEST, true /*allowIo*/); + handlerThread.start(); + return handlerThread; + } + @VisibleForTesting static class Injector { NativeWrapper createNativeWrapper() { @@ -306,7 +343,18 @@ public final class HintManagerService extends SystemService { public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) { FgThread.getHandler().post(() -> { synchronized (mCacheLock) { - mProcStatesCache.put(uid, procState); + if (powerhintThreadCleanup()) { + final boolean before = isUidForeground(uid); + mProcStatesCache.put(uid, procState); + final boolean after = isUidForeground(uid); + if (before != after) { + final Message msg = mCleanUpHandler.obtainMessage(EVENT_CLEAN_UP_UID, + uid); + mCleanUpHandler.sendMessageDelayed(msg, CLEAN_UP_UID_DELAY_MILLIS); + } + } else { + mProcStatesCache.put(uid, procState); + } } boolean shouldAllowUpdate = isUidForeground(uid); synchronized (mLock) { @@ -314,9 +362,10 @@ public final class HintManagerService extends SystemService { if (tokenMap == null) { return; } - for (ArraySet<AppHintSession> sessionSet : tokenMap.values()) { - for (AppHintSession s : sessionSet) { - s.onProcStateChanged(shouldAllowUpdate); + for (int i = tokenMap.size() - 1; i >= 0; i--) { + final ArraySet<AppHintSession> sessionSet = tokenMap.valueAt(i); + for (int j = sessionSet.size() - 1; j >= 0; j--) { + sessionSet.valueAt(j).onProcStateChanged(shouldAllowUpdate); } } } @@ -324,52 +373,237 @@ public final class HintManagerService extends SystemService { } } + final class CleanUpHandler extends Handler { + // status of processed tid used for caching + private static final int TID_NOT_CHECKED = 0; + private static final int TID_PASSED_CHECK = 1; + private static final int TID_EXITED = 2; + + CleanUpHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == EVENT_CLEAN_UP_UID) { + if (hasEqualMessages(msg.what, msg.obj)) { + removeEqualMessages(msg.what, msg.obj); + final Message newMsg = obtainMessage(msg.what, msg.obj); + sendMessageDelayed(newMsg, CLEAN_UP_UID_DELAY_MILLIS); + return; + } + final int uid = (int) msg.obj; + boolean isForeground = mUidObserver.isUidForeground(uid); + // store all sessions in a list and release the global lock + // we don't need to worry about stale data or racing as the session is synchronized + // itself and will perform its own closed status check in setThreads call + final List<AppHintSession> sessions; + synchronized (mLock) { + final ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap = + mActiveSessions.get(uid); + if (tokenMap == null || tokenMap.isEmpty()) { + return; + } + sessions = new ArrayList<>(tokenMap.size()); + for (int i = tokenMap.size() - 1; i >= 0; i--) { + final ArraySet<AppHintSession> set = tokenMap.valueAt(i); + for (int j = set.size() - 1; j >= 0; j--) { + sessions.add(set.valueAt(j)); + } + } + } + final long[] durationList = new long[sessions.size()]; + final int[] invalidTidCntList = new int[sessions.size()]; + final SparseIntArray checkedTids = new SparseIntArray(); + int[] totalTidCnt = new int[1]; + for (int i = sessions.size() - 1; i >= 0; i--) { + final AppHintSession session = sessions.get(i); + final long start = System.nanoTime(); + try { + final int invalidCnt = cleanUpSession(session, checkedTids, totalTidCnt); + final long elapsed = System.nanoTime() - start; + invalidTidCntList[i] = invalidCnt; + durationList[i] = elapsed; + } catch (Exception e) { + Slog.e(TAG, "Failed to clean up session " + session.mHalSessionPtr + + " for UID " + session.mUid); + } + } + logCleanUpMetrics(uid, invalidTidCntList, durationList, sessions.size(), + totalTidCnt[0], isForeground); + } + } + + private void logCleanUpMetrics(int uid, int[] count, long[] durationNsList, int sessionCnt, + int totalTidCnt, boolean isForeground) { + int maxInvalidTidCnt = Integer.MIN_VALUE; + int totalInvalidTidCnt = 0; + for (int i = 0; i < count.length; i++) { + totalInvalidTidCnt += count[i]; + maxInvalidTidCnt = Math.max(maxInvalidTidCnt, count[i]); + } + if (DEBUG || totalInvalidTidCnt > 0) { + Arrays.sort(durationNsList); + long totalDurationNs = 0; + for (int i = 0; i < durationNsList.length; i++) { + totalDurationNs += durationNsList[i]; + } + int totalDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(totalDurationNs); + int maxDurationUs = (int) TimeUnit.NANOSECONDS.toMicros( + durationNsList[durationNsList.length - 1]); + int minDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(durationNsList[0]); + int avgDurationUs = (int) TimeUnit.NANOSECONDS.toMicros( + totalDurationNs / durationNsList.length); + int th90DurationUs = (int) TimeUnit.NANOSECONDS.toMicros( + durationNsList[(int) (durationNsList.length * 0.9)]); + Slog.d(TAG, + "Invalid tid found for UID" + uid + " in " + totalDurationUs + "us:\n\t" + + "count(" + + " session: " + sessionCnt + + " totalTid: " + totalTidCnt + + " maxInvalidTid: " + maxInvalidTidCnt + + " totalInvalidTid: " + totalInvalidTidCnt + ")\n\t" + + "time per session(" + + " min: " + minDurationUs + "us" + + " max: " + maxDurationUs + "us" + + " avg: " + avgDurationUs + "us" + + " 90%: " + th90DurationUs + "us" + ")\n\t" + + "isForeground: " + isForeground); + } + } + + // This will check if each TID currently linked to the session still exists. If it's + // previously registered as not an isolated process, then it will run tkill(pid, tid, 0) to + // verify that it's still running under the same pid. Otherwise, it will run + // kill(tid, 0) to only check if it exists. The result will be cached in checkedTids + // map with tid as the key and checked status as value. + public int cleanUpSession(AppHintSession session, SparseIntArray checkedTids, int[] total) { + if (session.isClosed()) { + return 0; + } + final int pid = session.mPid; + final int[] tids = session.getTidsInternal(); + if (total != null && total.length == 1) { + total[0] += tids.length; + } + final IntArray filtered = new IntArray(tids.length); + for (int i = 0; i < tids.length; i++) { + int tid = tids[i]; + if (checkedTids.get(tid, 0) != TID_NOT_CHECKED) { + if (checkedTids.get(tid) == TID_PASSED_CHECK) { + filtered.add(tid); + } + continue; + } + // if it was registered as a non-isolated then we perform more restricted check + final boolean isNotIsolated; + synchronized (mNonIsolatedTidsLock) { + isNotIsolated = mNonIsolatedTids.containsKey(tid); + } + try { + if (isNotIsolated) { + Process.checkTid(pid, tid); + } else { + Process.checkPid(tid); + } + checkedTids.put(tid, TID_PASSED_CHECK); + filtered.add(tid); + } catch (NoSuchElementException e) { + checkedTids.put(tid, TID_EXITED); + } catch (Exception e) { + Slog.w(TAG, "Unexpected exception when checking TID " + tid + " under PID " + + pid + "(isolated: " + !isNotIsolated + ")", e); + // if anything unexpected happens then we keep it, but don't store it as checked + filtered.add(tid); + } + } + final int diff = tids.length - filtered.size(); + if (diff > 0) { + synchronized (session) { + // in case thread list is updated during the cleanup then we skip updating + // the session but just return the number for reporting purpose + final int[] newTids = session.getTidsInternal(); + if (newTids.length != tids.length) { + Slog.d(TAG, "Skipped cleaning up the session as new tids are added"); + return diff; + } + Arrays.sort(newTids); + Arrays.sort(tids); + if (!Arrays.equals(newTids, tids)) { + Slog.d(TAG, "Skipped cleaning up the session as new tids are updated"); + return diff; + } + Slog.d(TAG, "Cleaned up " + diff + " invalid tids for session " + + session.mHalSessionPtr + " with UID " + session.mUid + "\n\t" + + "before: " + Arrays.toString(tids) + "\n\t" + + "after: " + filtered); + final int[] filteredTids = filtered.toArray(); + if (filteredTids.length == 0) { + session.mShouldForcePause = true; + if (session.mUpdateAllowed) { + session.pause(); + } + } else { + session.setThreadsInternal(filteredTids, false); + } + } + } + return diff; + } + } + @VisibleForTesting IHintManager.Stub getBinderServiceInstance() { return mService; } // returns the first invalid tid or null if not found - private Integer checkTidValid(int uid, int tgid, int [] tids) { + private Integer checkTidValid(int uid, int tgid, int [] tids, IntArray nonIsolated) { // Make sure all tids belongs to the same UID (including isolated UID), // tids can belong to different application processes. List<Integer> isolatedPids = null; - for (int threadId : tids) { + for (int i = 0; i < tids.length; i++) { + int tid = tids[i]; final String[] procStatusKeys = new String[] { "Uid:", "Tgid:" }; long[] output = new long[procStatusKeys.length]; - Process.readProcLines("/proc/" + threadId + "/status", procStatusKeys, output); + Process.readProcLines("/proc/" + tid + "/status", procStatusKeys, output); int uidOfThreadId = (int) output[0]; int pidOfThreadId = (int) output[1]; - // use PID check for isolated processes, use UID check for non-isolated processes. - if (pidOfThreadId == tgid || uidOfThreadId == uid) { + // use PID check for non-isolated processes + if (nonIsolated != null && pidOfThreadId == tgid) { + nonIsolated.add(tid); + continue; + } + // use UID check for isolated processes. + if (uidOfThreadId == uid) { continue; } // Only call into AM if the tid is either isolated or invalid if (isolatedPids == null) { // To avoid deadlock, do not call into AMS if the call is from system. if (uid == Process.SYSTEM_UID) { - return threadId; + return tid; } isolatedPids = mAmInternal.getIsolatedProcesses(uid); if (isolatedPids == null) { - return threadId; + return tid; } } if (isolatedPids.contains(pidOfThreadId)) { continue; } - return threadId; + return tid; } return null; } private String formatTidCheckErrMsg(int callingUid, int[] tids, Integer invalidTid) { return "Tid" + invalidTid + " from list " + Arrays.toString(tids) - + " doesn't belong to the calling application" + callingUid; + + " doesn't belong to the calling application " + callingUid; } @VisibleForTesting @@ -387,7 +621,10 @@ public final class HintManagerService extends SystemService { final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid()); final long identity = Binder.clearCallingIdentity(); try { - final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids); + final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray(tids.length) + : null; + final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids, + nonIsolated); if (invalidTid != null) { final String errMsg = formatTidCheckErrMsg(callingUid, tids, invalidTid); Slogf.w(TAG, errMsg); @@ -396,6 +633,14 @@ public final class HintManagerService extends SystemService { long halSessionPtr = mNativeWrapper.halCreateHintSession(callingTgid, callingUid, tids, durationNanos); + if (powerhintThreadCleanup()) { + synchronized (mNonIsolatedTidsLock) { + for (int i = nonIsolated.size() - 1; i >= 0; i--) { + mNonIsolatedTids.putIfAbsent(nonIsolated.get(i), new ArraySet<>()); + mNonIsolatedTids.get(nonIsolated.get(i)).add(halSessionPtr); + } + } + } if (halSessionPtr == 0) { return null; } @@ -482,6 +727,7 @@ public final class HintManagerService extends SystemService { protected boolean mUpdateAllowed; protected int[] mNewThreadIds; protected boolean mPowerEfficient; + protected boolean mShouldForcePause; private enum SessionModes { POWER_EFFICIENCY, @@ -498,6 +744,7 @@ public final class HintManagerService extends SystemService { mTargetDurationNanos = durationNanos; mUpdateAllowed = true; mPowerEfficient = false; + mShouldForcePause = false; final boolean allowed = mUidObserver.isUidForeground(mUid); updateHintAllowed(allowed); try { @@ -511,7 +758,7 @@ public final class HintManagerService extends SystemService { @VisibleForTesting boolean updateHintAllowed(boolean allowed) { synchronized (this) { - if (allowed && !mUpdateAllowed) resume(); + if (allowed && !mUpdateAllowed && !mShouldForcePause) resume(); if (!allowed && mUpdateAllowed) pause(); mUpdateAllowed = allowed; return mUpdateAllowed; @@ -521,7 +768,7 @@ public final class HintManagerService extends SystemService { @Override public void updateTargetWorkDuration(long targetDurationNanos) { synchronized (this) { - if (mHalSessionPtr == 0 || !mUpdateAllowed) { + if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) { return; } Preconditions.checkArgument(targetDurationNanos > 0, "Expected" @@ -534,7 +781,7 @@ public final class HintManagerService extends SystemService { @Override public void reportActualWorkDuration(long[] actualDurationNanos, long[] timeStampNanos) { synchronized (this) { - if (mHalSessionPtr == 0 || !mUpdateAllowed) { + if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) { return; } Preconditions.checkArgument(actualDurationNanos.length != 0, "the count" @@ -581,12 +828,25 @@ public final class HintManagerService extends SystemService { if (sessionSet.isEmpty()) tokenMap.remove(mToken); if (tokenMap.isEmpty()) mActiveSessions.remove(mUid); } + if (powerhintThreadCleanup()) { + synchronized (mNonIsolatedTidsLock) { + final int[] tids = getTidsInternal(); + for (int tid : tids) { + if (mNonIsolatedTids.containsKey(tid)) { + mNonIsolatedTids.get(tid).remove(mHalSessionPtr); + if (mNonIsolatedTids.get(tid).isEmpty()) { + mNonIsolatedTids.remove(tid); + } + } + } + } + } } @Override public void sendHint(@PerformanceHintManager.Session.Hint int hint) { synchronized (this) { - if (mHalSessionPtr == 0 || !mUpdateAllowed) { + if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) { return; } Preconditions.checkArgument(hint >= 0, "the hint ID value should be" @@ -596,33 +856,60 @@ public final class HintManagerService extends SystemService { } public void setThreads(@NonNull int[] tids) { + setThreadsInternal(tids, true); + } + + private void setThreadsInternal(int[] tids, boolean checkTid) { + if (tids.length == 0) { + throw new IllegalArgumentException("Thread id list can't be empty."); + } + synchronized (this) { if (mHalSessionPtr == 0) { return; } - if (tids.length == 0) { - throw new IllegalArgumentException("Thread id list can't be empty."); - } - final int callingUid = Binder.getCallingUid(); - final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid()); - final long identity = Binder.clearCallingIdentity(); - try { - final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids); - if (invalidTid != null) { - final String errMsg = formatTidCheckErrMsg(callingUid, tids, invalidTid); - Slogf.w(TAG, errMsg); - throw new SecurityException(errMsg); - } - } finally { - Binder.restoreCallingIdentity(identity); - } if (!mUpdateAllowed) { Slogf.v(TAG, "update hint not allowed, storing tids."); mNewThreadIds = tids; + mShouldForcePause = false; return; } + if (checkTid) { + final int callingUid = Binder.getCallingUid(); + final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid()); + final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray() : null; + final long identity = Binder.clearCallingIdentity(); + try { + final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids, + nonIsolated); + if (invalidTid != null) { + final String errMsg = formatTidCheckErrMsg(callingUid, tids, + invalidTid); + Slogf.w(TAG, errMsg); + throw new SecurityException(errMsg); + } + if (powerhintThreadCleanup()) { + synchronized (mNonIsolatedTidsLock) { + for (int i = nonIsolated.size() - 1; i >= 0; i--) { + mNonIsolatedTids.putIfAbsent(nonIsolated.get(i), + new ArraySet<>()); + mNonIsolatedTids.get(nonIsolated.get(i)).add(mHalSessionPtr); + } + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } mNativeWrapper.halSetThreads(mHalSessionPtr, tids); mThreadIds = tids; + mNewThreadIds = null; + // if the update is allowed but the session is force paused by tid clean up, then + // it's waiting for this tid update to resume + if (mShouldForcePause) { + resume(); + mShouldForcePause = false; + } } } @@ -632,10 +919,24 @@ public final class HintManagerService extends SystemService { } } + @VisibleForTesting + int[] getTidsInternal() { + synchronized (this) { + return mNewThreadIds != null ? Arrays.copyOf(mNewThreadIds, mNewThreadIds.length) + : Arrays.copyOf(mThreadIds, mThreadIds.length); + } + } + + boolean isClosed() { + synchronized (this) { + return mHalSessionPtr == 0; + } + } + @Override public void setMode(int mode, boolean enabled) { synchronized (this) { - if (mHalSessionPtr == 0 || !mUpdateAllowed) { + if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) { return; } Preconditions.checkArgument(mode >= 0, "the mode Id value should be" @@ -650,13 +951,13 @@ public final class HintManagerService extends SystemService { @Override public void reportActualWorkDuration2(WorkDuration[] workDurations) { synchronized (this) { - if (mHalSessionPtr == 0 || !mUpdateAllowed) { + if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) { return; } Preconditions.checkArgument(workDurations.length != 0, "the count" + " of work durations shouldn't be 0."); - for (WorkDuration workDuration : workDurations) { - validateWorkDuration(workDuration); + for (int i = 0; i < workDurations.length; i++) { + validateWorkDuration(workDurations[i]); } mNativeWrapper.halReportActualWorkDuration(mHalSessionPtr, workDurations); } @@ -743,6 +1044,7 @@ public final class HintManagerService extends SystemService { pw.println(prefix + "SessionTIDs: " + Arrays.toString(mThreadIds)); pw.println(prefix + "SessionTargetDurationNanos: " + mTargetDurationNanos); pw.println(prefix + "SessionAllowed: " + mUpdateAllowed); + pw.println(prefix + "SessionForcePaused: " + mShouldForcePause); pw.println(prefix + "PowerEfficient: " + (mPowerEfficient ? "true" : "false")); } } diff --git a/services/core/java/com/android/server/power/hint/flags.aconfig b/services/core/java/com/android/server/power/hint/flags.aconfig new file mode 100644 index 000000000000..f4afcb141b19 --- /dev/null +++ b/services/core/java/com/android/server/power/hint/flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.power.hint" + +flag { + name: "powerhint_thread_cleanup" + namespace: "game" + description: "Feature flag for auto PowerHintSession dead thread cleanup" + bug: "296160319" +} diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig index b2e01c5f23f2..c42cceab55be 100644 --- a/services/core/java/com/android/server/power/stats/flags.aconfig +++ b/services/core/java/com/android/server/power/stats/flags.aconfig @@ -2,6 +2,7 @@ package: "com.android.server.power.optimization" flag { name: "power_monitor_api" + is_exported: true namespace: "backstage_power" description: "Feature flag for ODPM API" bug: "295027807" diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index f7c236afda20..2ff38616fce5 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -267,7 +267,7 @@ public interface StatusBarManagerInternal { void removeQsTile(ComponentName tile); /** - * Called when requested to enter desktop from an app. + * Called when requested to enter desktop from a focused app. */ - void enterDesktop(int displayId); + void moveFocusedTaskToDesktop(int displayId); } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 7b3e23776a55..cca5beb13405 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -838,15 +838,17 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } catch (RemoteException ex) { } } } + @Override - public void enterDesktop(int displayId) { + public void moveFocusedTaskToDesktop(int displayId) { IStatusBar bar = mBar; if (bar != null) { try { - bar.enterDesktop(displayId); + bar.moveFocusedTaskToDesktop(displayId); } catch (RemoteException ex) { } } } + @Override public void showMediaOutputSwitcher(String packageName) { IStatusBar bar = mBar; diff --git a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java index a25d67ab66af..f3d7dd19ecc2 100644 --- a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java +++ b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java @@ -24,8 +24,10 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.net.ConnectivityManager; import android.net.IpSecTransformState; import android.net.Network; +import android.net.vcn.Flags; import android.net.vcn.VcnManager; import android.os.Handler; import android.os.HandlerExecutor; @@ -71,6 +73,7 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor { @NonNull private final Handler mHandler; @NonNull private final PowerManager mPowerManager; + @NonNull private final ConnectivityManager mConnectivityManager; @NonNull private final Object mCancellationToken = new Object(); @NonNull private final PacketLossCalculator mPacketLossCalculator; @@ -98,6 +101,8 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor { mHandler = new Handler(getVcnContext().getLooper()); mPowerManager = getVcnContext().getContext().getSystemService(PowerManager.class); + mConnectivityManager = + getVcnContext().getContext().getSystemService(ConnectivityManager.class); mPacketLossCalculator = deps.getPacketLossCalculator(); @@ -313,6 +318,13 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor { } else { logInfo(logMsg); onValidationResultReceivedInternal(true /* isFailed */); + + if (Flags.validateNetworkOnIpsecLoss()) { + // Trigger re-validation of the underlying network; if it fails, the VCN will + // attempt to migrate away. + mConnectivityManager.reportNetworkConnectivity( + getNetwork(), false /* hasConnectivity */); + } } } diff --git a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java index 1704aa117a2b..4bacf3b8abe5 100644 --- a/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java +++ b/services/core/java/com/android/server/vcn/routeselection/NetworkMetricMonitor.java @@ -203,6 +203,11 @@ public abstract class NetworkMetricMonitor implements AutoCloseable { return mVcnContext; } + @NonNull + public Network getNetwork() { + return mNetwork; + } + // Override methods for AutoCloseable. Subclasses MUST call super when overriding this method @Override public void close() { diff --git a/services/core/java/com/android/server/webkit/flags.aconfig b/services/core/java/com/android/server/webkit/flags.aconfig index 1411acc4ab84..2afbcd6f101d 100644 --- a/services/core/java/com/android/server/webkit/flags.aconfig +++ b/services/core/java/com/android/server/webkit/flags.aconfig @@ -2,6 +2,7 @@ package: "android.webkit" flag { name: "update_service_v2" + is_exported: true namespace: "webview" description: "Using a new version of the WebView update service" bug: "308907090" diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 418998870f16..3f041cb48ee2 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -961,16 +961,37 @@ final class AccessibilityController { populateTransformationMatrix(windowState, matrix); Region touchableRegion = mTempRegion3; windowState.getTouchableRegion(touchableRegion); - Rect touchableFrame = mTempRect1; - touchableRegion.getBounds(touchableFrame); - RectF windowFrame = mTempRectF; - windowFrame.set(touchableFrame); - windowFrame.offset(-windowState.getFrame().left, - -windowState.getFrame().top); - matrix.mapRect(windowFrame); Region windowBounds = mTempRegion2; - windowBounds.set((int) windowFrame.left, (int) windowFrame.top, - (int) windowFrame.right, (int) windowFrame.bottom); + if (Flags.useWindowOriginalTouchableRegionWhenMagnificationRecomputeBounds()) { + // For b/323366243, if using the bounds from touchableRegion.getBounds, in + // non-magnifiable windowBounds computation, part of the non-touchableRegion + // may be included into nonMagnifiedBounds. This will make users lose + // the magnification control on mis-included areas. + // Therefore, to prevent the above issue, we change to use the window exact + // touchableRegion in magnificationRegion computation. + // Like the original approach, the touchableRegion is in non-magnified display + // space, so first we need to offset the region by the windowFrames bounds, then + // apply the transform matrix to the region to get the exact region in magnified + // display space. + // TODO: For a long-term plan, since touchable regions provided by WindowState + // doesn't actually reflect the real touchable regions on display, we should + // delete the WindowState dependency and migrate to use the touchableRegion + // from WindowInfoListener data. (b/330653961) + touchableRegion.translate(-windowState.getFrame().left, + -windowState.getFrame().top); + applyMatrixToRegion(matrix, touchableRegion); + windowBounds.set(touchableRegion); + } else { + Rect touchableFrame = mTempRect1; + touchableRegion.getBounds(touchableFrame); + RectF windowFrame = mTempRectF; + windowFrame.set(touchableFrame); + windowFrame.offset(-windowState.getFrame().left, + -windowState.getFrame().top); + matrix.mapRect(windowFrame); + windowBounds.set((int) windowFrame.left, (int) windowFrame.top, + (int) windowFrame.right, (int) windowFrame.bottom); + } // Only update new regions Region portionOfWindowAlreadyAccountedFor = mTempRegion3; portionOfWindowAlreadyAccountedFor.set(mMagnificationRegion); @@ -1066,6 +1087,30 @@ final class AccessibilityController { || windowType == TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY; } + private void applyMatrixToRegion(Matrix matrix, Region region) { + // Since Matrix does not support mapRegion api, so we follow the Matrix#mapRect logic + // to apply the matrix to the given region. + // In Matrix#mapRect, the internal calculation is applying the transform matrix to + // rect's 4 corner points with the below calculation. (see SkMatrix::mapPoints) + // |A B C| |x| Ax+By+C Dx+Ey+F + // |D E F| |y| = |Ax+By+C Dx+Ey+F Gx+Hy+I| = ------- , ------- + // |G H I| |1| Gx+Hy+I Gx+Hy+I + // For magnification usage, the matrix is created from + // WindowState#getTransformationMatrix. We can simplify the matrix calculation to be + // |scale 0 trans_x| |x| + // | 0 scale trans_y| |y| = (scale*x + trans_x, scale*y + trans_y) + // | 0 0 1 | |1| + // So, to follow the simplified matrix computation, we first scale the region with + // matrix.scale, then translate the region with matrix.trans_x and matrix.trans_y. + float[] transformArray = sTempFloats; + matrix.getValues(transformArray); + // For magnification transform matrix, the scale_x and scale_y are equal. + region.scale(transformArray[Matrix.MSCALE_X]); + region.translate( + (int) transformArray[Matrix.MTRANS_X], + (int) transformArray[Matrix.MTRANS_Y]); + } + private void populateWindowsOnScreen(SparseArray<WindowState> outWindows) { mTempLayer = 0; mDisplayContent.forAllWindows((w) -> { diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 59a56de16ce6..19f344996700 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -149,6 +149,10 @@ class ActivityMetricsLogger { private static final int WINDOW_STATE_MULTI_WINDOW = 4; private static final int WINDOW_STATE_INVALID = -1; + // These should match AppStartOccurred.MultiWindowLaunchType in the atoms.proto + private static final int MULTI_WINDOW_LAUNCH_TYPE_UNSPECIFIED = 0; + private static final int MULTI_WINDOW_LAUNCH_TYPE_APP_PAIR = 1; + /** * If a launching activity isn't visible within this duration when the device is sleeping, e.g. * keyguard is locked, its transition info will be dropped. @@ -329,6 +333,8 @@ class ActivityMetricsLogger { @Nullable Runnable mPendingFullyDrawn; /** Non-null if the trace is active. */ @Nullable String mLaunchTraceName; + /** Whether this transition info is for an activity that is a part of multi-window. */ + int mMultiWindowLaunchType = MULTI_WINDOW_LAUNCH_TYPE_UNSPECIFIED; /** @return Non-null if there will be a window drawn event for the launch. */ @Nullable @@ -477,6 +483,7 @@ class ActivityMetricsLogger { final int activityRecordIdHashCode; final boolean relaunched; final long timestampNs; + final int multiWindowLaunchType; private TransitionInfoSnapshot(TransitionInfo info) { this(info, info.mLastLaunchedActivity, INVALID_DELAY); @@ -507,6 +514,7 @@ class ActivityMetricsLogger { this.windowsFullyDrawnDelayMs = windowsFullyDrawnDelayMs; relaunched = info.mRelaunched; timestampNs = info.mLaunchingState.mStartRealtimeNs; + multiWindowLaunchType = info.mMultiWindowLaunchType; } @WaitResult.LaunchState int getLaunchState() { @@ -744,6 +752,10 @@ class ActivityMetricsLogger { return; } + // Look at all other transition infos and mark them as a split pair if they belong to + // adjacent tasks + updateSplitPairLaunches(newInfo); + if (DEBUG_METRICS) Slog.i(TAG, "notifyActivityLaunched successful"); // A new launch sequence has begun. Start tracking it. mTransitionInfoList.add(newInfo); @@ -769,6 +781,36 @@ class ActivityMetricsLogger { } } + /** + * Updates all transition infos including the given {@param info} if they are a part of a + * split pair launch. + */ + private void updateSplitPairLaunches(@NonNull TransitionInfo info) { + final Task launchedActivityTask = info.mLastLaunchedActivity.getTask(); + final Task adjacentToLaunchedTask = launchedActivityTask.getAdjacentTask(); + if (adjacentToLaunchedTask == null) { + // Not a part of a split pair + return; + } + for (int i = mTransitionInfoList.size() - 1; i >= 0; i--) { + final TransitionInfo otherInfo = mTransitionInfoList.get(i); + if (otherInfo == info) { + continue; + } + final Task otherTask = otherInfo.mLastLaunchedActivity.getTask(); + // The adjacent task is the split root in which activities are started + if (otherTask.isDescendantOf(adjacentToLaunchedTask)) { + if (DEBUG_METRICS) { + Slog.i(TAG, "Found adjacent tasks t1=" + launchedActivityTask.mTaskId + + " t2=" + otherTask.mTaskId); + } + // These tasks are adjacent, so mark them as such + info.mMultiWindowLaunchType = MULTI_WINDOW_LAUNCH_TYPE_APP_PAIR; + otherInfo.mMultiWindowLaunchType = MULTI_WINDOW_LAUNCH_TYPE_APP_PAIR; + } + } + } + private void scheduleCheckActivityToBeDrawnIfSleeping(@NonNull ActivityRecord r) { if (r.mDisplayContent.isSleeping()) { // It is unknown whether the activity can be drawn or not, e.g. it depends on the @@ -1168,7 +1210,8 @@ class ActivityMetricsLogger { packageState, false, // is_xr_activity firstLaunch, - 0L /* TODO: stoppedDuration */); + 0L /* TODO: stoppedDuration */, + info.multiWindowLaunchType); // Reset the stopped state to avoid reporting stopped again if (info.processRecord != null) { info.processRecord.setWasStoppedLogged(true); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 23f9743619e3..17e699668d14 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -11012,6 +11012,20 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } /** + * Returns the {@link #createTime} if the top window is the `base` window. Note that do not + * use the window creation time because the window could be re-created when the activity + * relaunched if configuration changed. + * <p> + * Otherwise, return the creation time of the top window. + */ + long getLastWindowCreateTime() { + final WindowState window = getWindow(win -> true); + return window != null && window.mAttrs.type != TYPE_BASE_APPLICATION + ? window.getCreateTime() + : createTime; + } + + /** * Adjust the source rect hint in {@link #pictureInPictureArgs} by window bounds since * it is relative to its root view (see also b/235599028). * It is caller's responsibility to make sure this is called exactly once when we update diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 060f1c8cfac0..6af496f4af24 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -5682,29 +5682,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { throw e; } - /** - * Sets the corresponding {@link DisplayArea} information for the process global - * configuration. To be called when we need to show IME on a different {@link DisplayArea} - * or display. - * - * @param pid The process id associated with the IME window. - * @param imeContainer The DisplayArea that contains the IME window. - */ - void onImeWindowSetOnDisplayArea(final int pid, @NonNull final DisplayArea imeContainer) { - if (pid == MY_PID || pid < 0) { - ProtoLog.w(WM_DEBUG_CONFIGURATION, - "Trying to update display configuration for system/invalid process."); - return; - } - final WindowProcessController process = mProcessMap.getProcess(pid); - if (process == null) { - ProtoLog.w(WM_DEBUG_CONFIGURATION, "Trying to update display " - + "configuration for invalid process, pid=%d", pid); - return; - } - process.registerDisplayAreaConfigurationListener(imeContainer); - } - @Override public void setRunningRemoteTransitionDelegate(IApplicationThread delegate) { final TransitionController controller = getTransitionController(); diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index e3ac35ca8f3b..48d78f5e497b 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -165,7 +165,7 @@ class BackNavigationController { } // Move focus to the top embedded window if possible - if (mWindowManagerService.moveFocusToTopEmbeddedWindow(window)) { + if (mWindowManagerService.moveFocusToAdjacentEmbeddedWindow(window)) { window = wmService.getFocusedWindowLocked(); if (window == null) { Slog.e(TAG, "New focused window is null, returning null."); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index eb1f052baac6..fe280cbcc205 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4171,11 +4171,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ void setInputMethodWindowLocked(WindowState win) { mInputMethodWindow = win; - // Update display configuration for IME process. - if (mInputMethodWindow != null) { - final int imePid = mInputMethodWindow.mSession.mPid; - mAtmService.onImeWindowSetOnDisplayArea(imePid, mImeWindowsContainer); - } mInsetsStateController.getImeSourceProvider().setWindowContainer(win, mDisplayPolicy.getImeSourceFrameProvider(), null); computeImeTarget(true /* updateImeTarget */); @@ -5102,7 +5097,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } finally { Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } - prepareSurfaces(); + if (!com.android.window.flags.Flags.removePrepareSurfaceInPlacement()) { + prepareSurfaces(); + } // This should be called after the insets have been dispatched to clients and we have // committed finish drawing windows. diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 30134d815fa6..e157318543f6 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -283,14 +283,14 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { int lastSyncSeqId, ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration, SurfaceControl outSurfaceControl, InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls, - Bundle outSyncSeqIdBundle) { + Bundle outBundle) { if (false) Slog.d(TAG_WM, ">>>>>> ENTERED relayout from " + Binder.getCallingPid()); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mRelayoutTag); int res = mService.relayoutWindow(this, window, attrs, requestedWidth, requestedHeight, viewFlags, flags, seq, lastSyncSeqId, outFrames, mergedConfiguration, outSurfaceControl, outInsetsState, - outActiveControls, outSyncSeqIdBundle); + outActiveControls, outBundle); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (false) Slog.d(TAG_WM, "<<<<<< EXITING relayout to " + Binder.getCallingPid()); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 55dc30cc37d5..18d2718437a6 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -1274,7 +1274,8 @@ class Task extends TaskFragment { if (!isLeafTaskFragment()) { final ActivityRecord top = topRunningActivity(); final ActivityRecord resumedActivity = getResumedActivity(); - if (resumedActivity != null && top.getTaskFragment() != this) { + if (resumedActivity != null + && (top.getTaskFragment() != this || !canBeResumed(resuming))) { // Pausing the resumed activity because it is occluded by other task fragment. if (startPausing(false /* uiSleeping*/, resuming, reason)) { someActivityPaused[0]++; @@ -3753,11 +3754,9 @@ class Task extends TaskFragment { // Boost the adjacent TaskFragment for dimmer if needed. final TaskFragment taskFragment = wc.asTaskFragment(); if (taskFragment != null && taskFragment.isEmbedded()) { - taskFragment.mDimmerSurfaceBoosted = false; final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment(); if (adjacentTf != null && adjacentTf.shouldBoostDimmer()) { adjacentTf.assignLayer(t, layer++); - adjacentTf.mDimmerSurfaceBoosted = true; } } @@ -6823,8 +6822,8 @@ class Task extends TaskFragment { * A decor surface is requested by a {@link TaskFragmentOrganizer} and is placed below children * windows in the Task except for own Activities and TaskFragments in fully trusted mode. The * decor surface is created and shared with the client app with - * {@link android.window.TaskFragmentOperation#OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE} and - * be removed with + * {@link android.window.TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE} + * and be removed with * {@link android.window.TaskFragmentOperation#OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE}. * * When boosted with diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 3cf561c1b62f..dc0e0341ee8b 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -216,9 +216,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { Dimmer mDimmer = Dimmer.DIMMER_REFACTOR ? new SmoothDimmer(this) : new LegacyDimmer(this); - /** {@code true} if the dimmer surface is boosted. {@code false} otherwise. */ - boolean mDimmerSurfaceBoosted; - /** Apply the dim layer on the embedded TaskFragment. */ static final int EMBEDDED_DIM_AREA_TASK_FRAGMENT = 0; diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 66c2e537ba9b..319e2b024f2f 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2468,7 +2468,15 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { for (WindowContainer<?> p = getAnimatableParent(wc); p != null; p = getAnimatableParent(p)) { final ChangeInfo parentChange = changes.get(p); - if (parentChange == null || !parentChange.hasChanged()) break; + if (parentChange == null) { + break; + } + if (!parentChange.hasChanged()) { + // In case the target is collected after the parent has been changed, it could + // be too late to snapshot the parent change. Skip to see if there is any + // parent window further up to be considered as change parent. + continue; + } if (p.mRemoteToken == null) { // Intermediate parents must be those that has window to be managed by Shell. continue; diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index acc63305055b..daf8129f1683 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -1068,9 +1068,9 @@ public abstract class WindowManagerInternal { public abstract void clearBlockedApps(); /** - * Moves the current focus to the top activity window if the top activity is embedded. + * Moves the current focus to the adjacent activity if it has the latest created window. */ - public abstract boolean moveFocusToTopEmbeddedWindowIfNeeded(); + public abstract boolean moveFocusToAdjacentEmbeddedActivityIfNeeded(); /** * Returns an instance of {@link ScreenCapture.ScreenshotHardwareBuffer} containing the current diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 207b1bbcea16..f09ef9643433 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -154,6 +154,7 @@ import static com.android.server.wm.WindowManagerServiceDumpProto.POLICY; import static com.android.server.wm.WindowManagerServiceDumpProto.ROOT_WINDOW_CONTAINER; import static com.android.server.wm.WindowManagerServiceDumpProto.WINDOW_FRAMES_VALID; import static com.android.window.flags.Flags.multiCrop; +import static com.android.window.flags.Flags.setScPropertiesInClient; import android.Manifest; import android.Manifest.permission; @@ -304,6 +305,7 @@ import android.view.WindowManagerPolicyConstants.PointerEventListener; import android.view.displayhash.DisplayHash; import android.view.displayhash.VerifiedDisplayHash; import android.view.inputmethod.ImeTracker; +import android.window.ActivityWindowInfo; import android.window.AddToSurfaceSyncGroupResult; import android.window.ClientWindowFrames; import android.window.IGlobalDragListener; @@ -794,6 +796,8 @@ public class WindowManagerService extends IWindowManager.Stub Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE); private final Uri mImmersiveModeConfirmationsUri = Settings.Secure.getUriFor(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS); + private final Uri mDisableSecureWindowsUri = + Settings.Secure.getUriFor(Settings.Secure.DISABLE_SECURE_WINDOWS); private final Uri mPolicyControlUri = Settings.Global.getUriFor(Settings.Global.POLICY_CONTROL); private final Uri mForceDesktopModeOnExternalDisplaysUri = Settings.Global.getUriFor( @@ -822,6 +826,8 @@ public class WindowManagerService extends IWindowManager.Stub UserHandle.USER_ALL); resolver.registerContentObserver(mImmersiveModeConfirmationsUri, false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(mDisableSecureWindowsUri, false, this, + UserHandle.USER_ALL); resolver.registerContentObserver(mPolicyControlUri, false, this, UserHandle.USER_ALL); resolver.registerContentObserver(mForceDesktopModeOnExternalDisplaysUri, false, this, UserHandle.USER_ALL); @@ -876,6 +882,11 @@ public class WindowManagerService extends IWindowManager.Stub return; } + if (mDisableSecureWindowsUri.equals(uri)) { + updateDisableSecureWindows(); + return; + } + @UpdateAnimationScaleMode final int mode; if (mWindowAnimationScaleUri.equals(uri)) { @@ -895,6 +906,7 @@ public class WindowManagerService extends IWindowManager.Stub void loadSettings() { updateSystemUiSettings(false /* handleChange */); updateMaximumObscuringOpacityForTouch(); + updateDisableSecureWindows(); } void updateMaximumObscuringOpacityForTouch() { @@ -977,6 +989,28 @@ public class WindowManagerService extends IWindowManager.Stub }); } } + + void updateDisableSecureWindows() { + if (!SystemProperties.getBoolean(SYSTEM_DEBUGGABLE, false)) { + return; + } + + final boolean disableSecureWindows; + try { + disableSecureWindows = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.DISABLE_SECURE_WINDOWS, 0) != 0; + } catch (Settings.SettingNotFoundException e) { + return; + } + if (mDisableSecureWindows == disableSecureWindows) { + return; + } + + synchronized (mGlobalLock) { + mDisableSecureWindows = disableSecureWindows; + mRoot.refreshSecureSurfaceState(); + } + } } PowerManager mPowerManager; @@ -1115,6 +1149,8 @@ public class WindowManagerService extends IWindowManager.Stub private final ScreenRecordingCallbackController mScreenRecordingCallbackController; + private volatile boolean mDisableSecureWindows = false; + public static WindowManagerService main(final Context context, final InputManagerService im, final boolean showBootMsgs, WindowManagerPolicy policy, ActivityTaskManagerService atm) { @@ -2213,7 +2249,7 @@ public class WindowManagerService extends IWindowManager.Stub int lastSyncSeqId, ClientWindowFrames outFrames, MergedConfiguration outMergedConfiguration, SurfaceControl outSurfaceControl, InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls, - Bundle outSyncIdBundle) { + Bundle outBundle) { if (outActiveControls != null) { outActiveControls.set(null); } @@ -2328,9 +2364,12 @@ public class WindowManagerService extends IWindowManager.Stub updateNonSystemOverlayWindowsVisibilityIfNeeded( win, win.mWinAnimator.getShown()); } - if ((attrChanges & (WindowManager.LayoutParams.PRIVATE_FLAGS_CHANGED)) != 0) { - winAnimator.setColorSpaceAgnosticLocked((win.mAttrs.privateFlags - & WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) != 0); + if (!setScPropertiesInClient()) { + if ((attrChanges & (WindowManager.LayoutParams.PRIVATE_FLAGS_CHANGED)) != 0) { + winAnimator.setColorSpaceAgnosticLocked((win.mAttrs.privateFlags + & WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) + != 0); + } } // See if the DisplayWindowPolicyController wants to keep the activity on the window if (displayContent.mDwpcHelper.hasController() @@ -2544,6 +2583,13 @@ public class WindowManagerService extends IWindowManager.Stub if (outFrames != null && outMergedConfiguration != null) { win.fillClientWindowFramesAndConfiguration(outFrames, outMergedConfiguration, false /* useLatestConfig */, shouldRelayout); + if (Flags.activityWindowInfoFlag() && outBundle != null + && win.mActivityRecord != null) { + final ActivityWindowInfo activityWindowInfo = win.mActivityRecord + .getActivityWindowInfo(); + outBundle.putParcelable(IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO, + activityWindowInfo); + } // Set resize-handled here because the values are sent back to the client. win.onResizeHandled(); @@ -2573,7 +2619,7 @@ public class WindowManagerService extends IWindowManager.Stub win.isVisible() /* visible */, false /* removed */); } - if (outSyncIdBundle != null) { + if (outBundle != null) { final int maybeSyncSeqId; if (win.syncNextBuffer() && viewVisibility == View.VISIBLE && win.mSyncSeqId > lastSyncSeqId) { @@ -2582,7 +2628,7 @@ public class WindowManagerService extends IWindowManager.Stub } else { maybeSyncSeqId = -1; } - outSyncIdBundle.putInt("seqid", maybeSyncSeqId); + outBundle.putInt(IWindowSession.KEY_RELAYOUT_BUNDLE_SEQID, maybeSyncSeqId); } if (configChanged) { @@ -6897,6 +6943,7 @@ public class WindowManagerService extends IWindowManager.Stub pw.print(mLastFinishedFreezeSource); } pw.println(); + pw.print(" mDisableSecureWindows="); pw.println(mDisableSecureWindows); mInputManagerCallback.dump(pw, " "); mSnapshotController.dump(pw, " "); @@ -8700,14 +8747,14 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public boolean moveFocusToTopEmbeddedWindowIfNeeded() { + public boolean moveFocusToAdjacentEmbeddedActivityIfNeeded() { synchronized (mGlobalLock) { final WindowState focusedWindow = getFocusedWindow(); if (focusedWindow == null) { return false; } - if (moveFocusToTopEmbeddedWindow(focusedWindow)) { + if (moveFocusToAdjacentEmbeddedWindow(focusedWindow)) { // Sync the input transactions to ensure the input focus updates as well. syncInputTransactions(false); return true; @@ -9219,9 +9266,10 @@ public class WindowManagerService extends IWindowManager.Stub } /** - * Move focus to the top embedded window if possible. + * Move focus to the adjacent embedded activity if the adjacent activity is more recently + * created or has a window more recently added. */ - boolean moveFocusToTopEmbeddedWindow(@NonNull WindowState focusedWindow) { + boolean moveFocusToAdjacentEmbeddedWindow(@NonNull WindowState focusedWindow) { final TaskFragment taskFragment = focusedWindow.getTaskFragment(); if (taskFragment == null) { // Skip if not an Activity window. @@ -9233,31 +9281,25 @@ public class WindowManagerService extends IWindowManager.Stub return false; } - if (taskFragment.mDimmerSurfaceBoosted) { - // Skip if the TaskFragment currently has dimmer surface boosted. - return false; - } - - final ActivityRecord topActivity = - taskFragment.getTask().topRunningActivity(true /* focusableOnly */); - if (topActivity == null || topActivity == focusedWindow.mActivityRecord) { - // Skip if the focused activity is already the top-most activity on the Task. + if (!focusedWindow.mActivityRecord.isEmbedded()) { + // Skip if the focused activity is not embedded return false; } - if (!topActivity.isEmbedded()) { - // Skip if the top activity is not embedded + final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); + final ActivityRecord adjacentTopActivity = + adjacentTaskFragment != null ? adjacentTaskFragment.topRunningActivity() : null; + if (adjacentTopActivity == null) { return false; } - final TaskFragment topTaskFragment = topActivity.getTaskFragment(); - if (topTaskFragment.isIsolatedNav() - && taskFragment.getAdjacentTaskFragment() == topTaskFragment) { - // Skip if the top TaskFragment is adjacent to current focus and is set to isolated nav. + if (adjacentTopActivity.getLastWindowCreateTime() + < focusedWindow.mActivityRecord.getLastWindowCreateTime()) { + // Skip if the current focus activity has more recently active window. return false; } - moveFocusToActivity(topActivity); + moveFocusToActivity(adjacentTopActivity); return !focusedWindow.isFocused(); } @@ -10073,4 +10115,8 @@ public class WindowManagerService extends IWindowManager.Stub mDragDropController.setGlobalDragListener(listener); } } + + boolean getDisableSecureWindows() { + return mDisableSecureWindows; + } } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index d967cde84cbf..14ec41f072dd 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -23,7 +23,7 @@ import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; -import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK; @@ -1558,7 +1558,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } break; } - case OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE: { + case OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE: { taskFragment.getTask().moveOrCreateDecorSurfaceFor(taskFragment); break; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 2b337aed5b87..37b2d0e82366 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -240,6 +240,7 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.view.inputmethod.ImeTracker; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.OnBackInvokedCallbackInfo; @@ -364,6 +365,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP private boolean mDragResizing; private boolean mDragResizingChangeReported = true; private boolean mRedrawForSyncReported = true; + private long mCreateTime = System.currentTimeMillis(); /** * Used to assosciate a given set of state changes sent from MSG_RESIZED @@ -1714,6 +1716,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP : DEFAULT_DISPATCHING_TIMEOUT_MILLIS; } + long getCreateTime() { + return mCreateTime; + } + /** * Returns true if, at any point, the application token associated with this window has actually * displayed any windows. This is most useful with the "starting up" window to determine if any @@ -1893,6 +1899,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } boolean isSecureLocked() { + if (mWmService.getDisableSecureWindows()) { + return false; + } + if ((mAttrs.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0) { return true; } @@ -3687,19 +3697,32 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP markRedrawForSyncReported(); + // App window resize may trigger Activity#onConfigurationChanged, so we need to update + // ActivityWindowInfo as well. + final IBinder activityToken; + final ActivityWindowInfo activityWindowInfo; + if (Flags.activityWindowInfoFlag() && mActivityRecord != null) { + activityToken = mActivityRecord.token; + activityWindowInfo = mActivityRecord.getActivityWindowInfo(); + } else { + activityToken = null; + activityWindowInfo = null; + } + if (Flags.bundleClientTransactionFlag()) { getProcess().scheduleClientTransactionItem( WindowStateResizeItem.obtain(mClient, mClientWindowFrames, reportDraw, mLastReportedConfiguration, getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId, - syncWithBuffers ? mSyncSeqId : -1, isDragResizing)); + syncWithBuffers ? mSyncSeqId : -1, isDragResizing, + activityToken, activityWindowInfo)); onResizePostDispatched(drawPending, prevRotation, displayId); } else { // TODO(b/301870955): cleanup after launch try { mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration, getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId, - syncWithBuffers ? mSyncSeqId : -1, isDragResizing); + syncWithBuffers ? mSyncSeqId : -1, isDragResizing, activityWindowInfo); onResizePostDispatched(drawPending, prevRotation, displayId); } catch (RemoteException e) { // Cancel orientation change of this window to avoid blocking unfreeze display. diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index 7f7c2493cd68..a242d4242388 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -45,6 +45,7 @@ import static com.android.server.wm.WindowStateAnimatorProto.DRAW_STATE; import static com.android.server.wm.WindowStateAnimatorProto.SURFACE; import static com.android.server.wm.WindowStateAnimatorProto.SYSTEM_DECOR_RECT; import static com.android.window.flags.Flags.secureWindowState; +import static com.android.window.flags.Flags.setScPropertiesInClient; import android.content.Context; import android.graphics.PixelFormat; @@ -311,8 +312,10 @@ class WindowStateAnimator { mSurfaceController = new WindowSurfaceController(attrs.getTitle().toString(), format, flags, this, attrs.type); - mSurfaceController.setColorSpaceAgnostic(w.getPendingTransaction(), - (attrs.privateFlags & LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) != 0); + if (!setScPropertiesInClient()) { + mSurfaceController.setColorSpaceAgnostic(w.getPendingTransaction(), + (attrs.privateFlags & LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC) != 0); + } w.setHasSurface(true); // The surface instance is changed. Make sure the input info can be applied to the diff --git a/services/core/jni/com_android_server_am_OomConnection.cpp b/services/core/jni/com_android_server_am_OomConnection.cpp index 49a3ad35649b..054937fc683e 100644 --- a/services/core/jni/com_android_server_am_OomConnection.cpp +++ b/services/core/jni/com_android_server_am_OomConnection.cpp @@ -44,6 +44,12 @@ static MemEventListener memevent_listener(MemEventClient::AMS); * @throws java.lang.RuntimeException */ static jobjectArray android_server_am_OomConnection_waitOom(JNIEnv* env, jobject) { + if (!memevent_listener.ok()) { + memevent_listener.deregisterAllEvents(); + jniThrowRuntimeException(env, "Failed to initialize memevents listener"); + return nullptr; + } + if (!memevent_listener.registerEvent(MEM_EVENT_OOM_KILL)) { memevent_listener.deregisterAllEvents(); jniThrowRuntimeException(env, "listener failed to register to OOM events"); diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 610fcb5962c8..70224db061c7 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -143,6 +143,7 @@ static struct { jmethodID getTouchCalibrationForInputDevice; jmethodID notifyDropWindow; jmethodID getParentSurfaceForPointers; + jmethodID getPackageUid; } gServiceClassInfo; static struct { @@ -362,6 +363,7 @@ public: void notifyDropWindow(const sp<IBinder>& token, float x, float y) override; void notifyDeviceInteraction(int32_t deviceId, nsecs_t timestamp, const std::set<gui::Uid>& uids) override; + gui::Uid getPackageUid(std::string package) override; /* --- PointerControllerPolicyInterface implementation --- */ @@ -1116,6 +1118,21 @@ void NativeInputManager::notifyDeviceInteraction(int32_t deviceId, nsecs_t times mInputManager->getMetricsCollector().notifyDeviceInteraction(deviceId, timestamp, uids); } +gui::Uid NativeInputManager::getPackageUid(std::string package) { + ATRACE_CALL(); + JNIEnv* env = jniEnv(); + ScopedLocalFrame localFrame(env); + + ScopedLocalRef<jstring> javaPackage(env, env->NewStringUTF(package.c_str())); + const jint uid = + env->CallIntMethod(mServiceObj, gServiceClassInfo.getPackageUid, javaPackage.get()); + if (checkAndClearExceptionFromCallback(env, "getPackageUid")) { + LOG(FATAL) << __func__ << ": Failed to get UID for package: " << package; + } + + return gui::Uid{static_cast<uint32_t>(uid)}; +} + void NativeInputManager::notifySensorEvent(int32_t deviceId, InputDeviceSensorType sensorType, InputDeviceSensorAccuracy accuracy, nsecs_t timestamp, const std::vector<float>& values) { @@ -3101,6 +3118,8 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.getParentSurfaceForPointers, clazz, "getParentSurfaceForPointers", "(I)J"); + GET_METHOD_ID(gServiceClassInfo.getPackageUid, clazz, "getPackageUid", "(Ljava/lang/String;)I"); + // InputDevice FIND_CLASS(gInputDeviceClassInfo.clazz, "android/view/InputDevice"); diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index d0df2b20721b..1f5451813dae 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -162,6 +162,10 @@ <xs:element type="usiVersion" name="usiVersion"> <xs:annotation name="final"/> </xs:element> + <xs:element type="lowBrightnessMode" name="lowBrightness"> + <xs:attribute name="enabled" type="xs:boolean" use="optional"/> + <xs:annotation name="final"/> + </xs:element> <!-- Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode. This must be a non-negative decimal within the range defined by the first and the last brightness value in screenBrightnessMap. --> @@ -172,6 +176,7 @@ <xs:element type="idleScreenRefreshRateTimeout" name="idleScreenRefreshRateTimeout" minOccurs="0"> <xs:annotation name="final"/> </xs:element> + </xs:sequence> </xs:complexType> </xs:element> @@ -216,6 +221,21 @@ </xs:restriction> </xs:simpleType> + <xs:complexType name="lowBrightnessMode"> + <xs:sequence> + <xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1" + maxOccurs="1"> + </xs:element> + <xs:element name="nits" type="xs:float" maxOccurs="unbounded"> + </xs:element> + <xs:element name="backlight" type="xs:float" maxOccurs="unbounded"> + </xs:element> + <xs:element name="brightness" type="xs:float" maxOccurs="unbounded"> + </xs:element> + </xs:sequence> + <xs:attribute name="enabled" type="xs:boolean" use="optional"/> + </xs:complexType> + <xs:complexType name="highBrightnessMode"> <xs:all> <xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1" diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index 00dc90828d90..c39c3d7ee7c6 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -113,6 +113,7 @@ package com.android.server.display.config { method public com.android.server.display.config.HighBrightnessMode getHighBrightnessMode(); method public final com.android.server.display.config.IdleScreenRefreshRateTimeout getIdleScreenRefreshRateTimeout(); method public final com.android.server.display.config.SensorDetails getLightSensor(); + method public final com.android.server.display.config.LowBrightnessMode getLowBrightness(); method public com.android.server.display.config.LuxThrottling getLuxThrottling(); method @Nullable public final String getName(); method public com.android.server.display.config.PowerThrottlingConfig getPowerThrottlingConfig(); @@ -149,6 +150,7 @@ package com.android.server.display.config { method public void setHighBrightnessMode(com.android.server.display.config.HighBrightnessMode); method public final void setIdleScreenRefreshRateTimeout(com.android.server.display.config.IdleScreenRefreshRateTimeout); method public final void setLightSensor(com.android.server.display.config.SensorDetails); + method public final void setLowBrightness(com.android.server.display.config.LowBrightnessMode); method public void setLuxThrottling(com.android.server.display.config.LuxThrottling); method public final void setName(@Nullable String); method public void setPowerThrottlingConfig(com.android.server.display.config.PowerThrottlingConfig); @@ -248,6 +250,17 @@ package com.android.server.display.config { method public java.util.List<java.math.BigInteger> getItem(); } + public class LowBrightnessMode { + ctor public LowBrightnessMode(); + method public java.util.List<java.lang.Float> getBacklight(); + method public java.util.List<java.lang.Float> getBrightness(); + method public boolean getEnabled(); + method public java.util.List<java.lang.Float> getNits(); + method public java.math.BigDecimal getTransitionPoint(); + method public void setEnabled(boolean); + method public void setTransitionPoint(java.math.BigDecimal); + } + public class LuxThrottling { ctor public LuxThrottling(); method @NonNull public final java.util.List<com.android.server.display.config.BrightnessLimitMap> getBrightnessLimitMap(); diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index 173cb36a1a34..cac42b17553a 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -112,7 +112,8 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), /*defaultProviderId=*/flattenedPrimaryProviders, /*isShowAllOptionsRequested=*/ false), - providerDataList); + providerDataList, + mRequestSessionMetric); mClientCallback.onPendingIntent(mPendingIntent); } catch (RemoteException e) { mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index f5e1e41dbae4..24f66977ee90 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.credentials.CredentialManager; import android.credentials.CredentialProviderInfo; import android.credentials.selection.DisabledProviderData; +import android.credentials.selection.IntentCreationResult; import android.credentials.selection.IntentFactory; import android.credentials.selection.ProviderData; import android.credentials.selection.RequestInfo; @@ -37,6 +38,8 @@ import android.os.ResultReceiver; import android.os.UserHandle; import android.service.credentials.CredentialProviderInfoFactory; +import com.android.server.credentials.metrics.RequestSessionMetric; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -159,7 +162,8 @@ public class CredentialManagerUi { * @param providerDataList the list of provider data from remote providers */ public PendingIntent createPendingIntent( - RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) { + RequestInfo requestInfo, ArrayList<ProviderData> providerDataList, + RequestSessionMetric requestSessionMetric) { List<CredentialProviderInfo> allProviders = CredentialProviderInfoFactory.getCredentialProviderServices( mContext, @@ -174,10 +178,12 @@ public class CredentialManagerUi { .map(disabledProvider -> new DisabledProviderData( disabledProvider.getComponentName().flattenToString())).toList(); - Intent intent; - intent = IntentFactory.createCredentialSelectorIntent( - mContext, requestInfo, providerDataList, - new ArrayList<>(disabledProviderDataList), mResultReceiver); + IntentCreationResult intentCreationResult = IntentFactory + .createCredentialSelectorIntentForCredMan(mContext, requestInfo, providerDataList, + new ArrayList<>(disabledProviderDataList), mResultReceiver); + requestSessionMetric.collectUiConfigurationResults( + mContext, intentCreationResult, mUserId); + Intent intent = intentCreationResult.getIntent(); intent.setAction(UUID.randomUUID().toString()); //TODO: Create unique pending intent using request code and cancel any pre-existing pending // intents @@ -197,10 +203,15 @@ public class CredentialManagerUi { * of the pinned entry. * * @param requestInfo the information about the request + * @param requestSessionMetric the metric object for logging */ - public Intent createIntentForAutofill(RequestInfo requestInfo) { - return IntentFactory.createCredentialSelectorIntentForAutofill( - mContext, requestInfo, new ArrayList<>(), - mResultReceiver); + public Intent createIntentForAutofill(RequestInfo requestInfo, + RequestSessionMetric requestSessionMetric) { + IntentCreationResult intentCreationResult = IntentFactory + .createCredentialSelectorIntentForAutofill(mContext, requestInfo, new ArrayList<>(), + mResultReceiver); + requestSessionMetric.collectUiConfigurationResults( + mContext, intentCreationResult, mUserId); + return intentCreationResult.getIntent(); } } diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java index eff53de75ff4..fd2a9a20640b 100644 --- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java @@ -122,7 +122,8 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ mRequestId, mClientRequest, mClientAppInfo.getPackageName(), PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(), Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), - /*isShowAllOptionsRequested=*/ true)); + /*isShowAllOptionsRequested=*/ true), + mRequestSessionMetric); List<GetCredentialProviderData> candidateProviderDataList = new ArrayList<>(); for (ProviderData providerData : providerDataList) { diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index 6513ae1af369..d55d8effd381 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -111,7 +111,8 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, Manifest.permission .CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), /*isShowAllOptionsRequested=*/ false), - providerDataList); + providerDataList, + mRequestSessionMetric); mClientCallback.onPendingIntent(mPendingIntent); } catch (RemoteException e) { mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index bdea4f9d2baa..16bf17781eea 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -16,6 +16,7 @@ package com.android.server.credentials; +import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; @@ -68,17 +69,27 @@ public class MetricUtilities { * * @return the uid of a given package */ - protected static int getPackageUid(Context context, ComponentName componentName) { - int sessUid = -1; + protected static int getPackageUid(Context context, ComponentName componentName, + @UserIdInt int userId) { + if (componentName == null) { + return -1; + } + return getPackageUid(context, componentName.getPackageName(), userId); + } + + /** Returns the package uid, or -1 if not found. */ + public static int getPackageUid(Context context, String packageName, + @UserIdInt int userId) { + if (packageName == null) { + return -1; + } try { - // Only for T and above, which is fine for our use case - sessUid = context.getPackageManager().getApplicationInfo( - componentName.getPackageName(), - PackageManager.ApplicationInfoFlags.of(0)).uid; + return context.getPackageManager().getPackageUidAsUser(packageName, + PackageManager.PackageInfoFlags.of(0), userId); } catch (Throwable t) { - Slog.i(TAG, "Couldn't find required uid"); + Slog.i(TAG, "Couldn't find uid for " + packageName + ": " + t); + return -1; } - return sessUid; } /** diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java index 6e8f7c8d7722..e4b5c776301e 100644 --- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java @@ -193,7 +193,8 @@ public class PrepareGetRequestSession extends GetRequestSession { PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(), Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), /*isShowAllOptionsRequested=*/ false), - providerDataList); + providerDataList, + mRequestSessionMetric); } else { return null; } diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index c16e2327abfb..dfc08f04386e 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -153,7 +153,7 @@ public abstract class ProviderSession<T, R> mUserId = userId; mComponentName = componentName; mRemoteCredentialService = remoteCredentialService; - mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName); + mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName, userId); mProviderSessionMetric = new ProviderSessionMetric( ((RequestSession) mCallbacks).mRequestSessionMetric.getSessionIdTrackTwo()); } diff --git a/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java b/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java index 2fd3a868369d..80ce354c4972 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java +++ b/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java @@ -22,7 +22,12 @@ import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FIN import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SPECIFIED_BUT_NOT_FOUND; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SPECIFIED_BUT_NOT_ENABLED; +import android.credentials.selection.IntentCreationResult; +/** + * Result of attempting to use the config_oemCredentialManagerDialogComponent as the Credential + * Manager UI. + */ public enum OemUiUsageStatus { UNKNOWN(CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_UNKNOWN), SUCCESS(CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SUCCESS), @@ -39,4 +44,21 @@ public enum OemUiUsageStatus { public int getLoggingInt() { return mLoggingInt; } + + /** Factory method. */ + public static OemUiUsageStatus createFrom(IntentCreationResult.OemUiUsageStatus from) { + switch (from) { + case UNKNOWN: + return OemUiUsageStatus.UNKNOWN; + case SUCCESS: + return OemUiUsageStatus.SUCCESS; + case OEM_UI_CONFIG_NOT_SPECIFIED: + return OemUiUsageStatus.FAILURE_NOT_SPECIFIED; + case OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND: + return OemUiUsageStatus.FAILURE_SPECIFIED_BUT_NOT_FOUND; + case OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED: + return OemUiUsageStatus.FAILURE_SPECIFIED_BUT_NOT_ENABLED; + } + return OemUiUsageStatus.UNKNOWN; + } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java index a77bd3e280dd..619a56846e95 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java @@ -30,9 +30,12 @@ import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL; import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL_VIA_REGISTRY; import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.content.ComponentName; +import android.content.Context; import android.credentials.CreateCredentialRequest; import android.credentials.GetCredentialRequest; +import android.credentials.selection.IntentCreationResult; import android.credentials.selection.UserSelectionDialogResult; import android.util.Slog; @@ -270,6 +273,21 @@ public class RequestSessionMetric { } } + /** Log results of the device Credential Manager UI configuration. */ + public void collectUiConfigurationResults(Context context, IntentCreationResult result, + @UserIdInt int userId) { + try { + mChosenProviderFinalPhaseMetric.setOemUiUid(MetricUtilities.getPackageUid( + context, result.getOemUiPackageName(), userId)); + mChosenProviderFinalPhaseMetric.setFallbackUiUid(MetricUtilities.getPackageUid( + context, result.getFallbackUiPackageName(), userId)); + mChosenProviderFinalPhaseMetric.setOemUiUsageStatus( + OemUiUsageStatus.createFrom(result.getOemUiUsageStatus())); + } catch (Exception e) { + Slog.w(TAG, "Unexpected error during ui configuration result collection: " + e); + } + } + /** * Allows encapsulating the overall final phase metric status from the chosen and final * provider. diff --git a/services/devicepolicy/Android.bp b/services/devicepolicy/Android.bp index 8dfa685bf6ff..da965bb02460 100644 --- a/services/devicepolicy/Android.bp +++ b/services/devicepolicy/Android.bp @@ -24,5 +24,6 @@ java_library_static { "app-compat-annotations", "service-permission.stubs.system_server", "device_policy_aconfig_flags_lib", + "androidx.annotation_annotation", ], } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java b/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java index f3b164c6501c..94c137444ede 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java @@ -25,15 +25,16 @@ import static android.app.admin.DevicePolicyManager.REQUIRED_APP_MANAGED_USER; import static android.content.pm.PackageManager.GET_META_DATA; import static com.android.internal.util.Preconditions.checkArgument; -import static com.android.internal.util.Preconditions.checkNotNull; -import static com.android.server.devicepolicy.DevicePolicyManagerService.dumpResources; +import static com.android.server.devicepolicy.DevicePolicyManagerService.dumpApps; import static java.util.Objects.requireNonNull; +import android.annotation.ArrayRes; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.admin.DeviceAdminReceiver; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.app.role.RoleManager; import android.content.ComponentName; import android.content.Context; @@ -67,13 +68,16 @@ public class OverlayPackagesProvider { protected static final String TAG = "OverlayPackagesProvider"; private static final Map<String, String> sActionToMetadataKeyMap = new HashMap<>(); - { + + static { sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_USER, REQUIRED_APP_MANAGED_USER); sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_PROFILE, REQUIRED_APP_MANAGED_PROFILE); sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_DEVICE, REQUIRED_APP_MANAGED_DEVICE); } + private static final Set<String> sAllowedActions = new HashSet<>(); - { + + static { sAllowedActions.add(ACTION_PROVISION_MANAGED_USER); sAllowedActions.add(ACTION_PROVISION_MANAGED_PROFILE); sAllowedActions.add(ACTION_PROVISION_MANAGED_DEVICE); @@ -83,8 +87,13 @@ public class OverlayPackagesProvider { private final Context mContext; private final Injector mInjector; + private final RecursiveStringArrayResourceResolver mRecursiveStringArrayResourceResolver; + public OverlayPackagesProvider(Context context) { - this(context, new DefaultInjector()); + this( + context, + new DefaultInjector(), + new RecursiveStringArrayResourceResolver(context.getResources())); } @VisibleForTesting @@ -113,8 +122,8 @@ public class OverlayPackagesProvider { public String getDevicePolicyManagementRoleHolderPackageName(Context context) { return Binder.withCleanCallingIdentity(() -> { RoleManager roleManager = context.getSystemService(RoleManager.class); - List<String> roleHolders = - roleManager.getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT); + List<String> roleHolders = roleManager.getRoleHolders( + RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT); if (roleHolders.isEmpty()) { return null; } @@ -124,17 +133,20 @@ public class OverlayPackagesProvider { } @VisibleForTesting - OverlayPackagesProvider(Context context, Injector injector) { + OverlayPackagesProvider(Context context, Injector injector, + RecursiveStringArrayResourceResolver recursiveStringArrayResourceResolver) { mContext = context; - mPm = checkNotNull(context.getPackageManager()); - mInjector = checkNotNull(injector); + mPm = requireNonNull(context.getPackageManager()); + mInjector = requireNonNull(injector); + mRecursiveStringArrayResourceResolver = requireNonNull( + recursiveStringArrayResourceResolver); } /** * Computes non-required apps. All the system apps with a launcher that are not in * the required set of packages, and all mainline modules that are not declared as required * via metadata in their manifests, will be considered as non-required apps. - * + * <p> * Note: If an app is mistakenly listed as both required and disallowed, it will be treated as * disallowed. * @@ -176,12 +188,12 @@ public class OverlayPackagesProvider { /** * Returns a subset of {@code packageNames} whose packages are mainline modules declared as * required apps via their app metadata. + * * @see DevicePolicyManager#REQUIRED_APP_MANAGED_USER * @see DevicePolicyManager#REQUIRED_APP_MANAGED_DEVICE * @see DevicePolicyManager#REQUIRED_APP_MANAGED_PROFILE */ - private Set<String> getRequiredAppsMainlineModules( - Set<String> packageNames, + private Set<String> getRequiredAppsMainlineModules(Set<String> packageNames, String provisioningAction) { final Set<String> result = new HashSet<>(); for (String packageName : packageNames) { @@ -225,8 +237,8 @@ public class OverlayPackagesProvider { } private boolean isApkInApexMainlineModule(String packageName) { - final String apexPackageName = - mInjector.getActiveApexPackageNameContainingPackage(packageName); + final String apexPackageName = mInjector.getActiveApexPackageNameContainingPackage( + packageName); return apexPackageName != null; } @@ -274,112 +286,94 @@ public class OverlayPackagesProvider { } private Set<String> getRequiredAppsSet(String provisioningAction) { - final int resId; - switch (provisioningAction) { - case ACTION_PROVISION_MANAGED_USER: - resId = R.array.required_apps_managed_user; - break; - case ACTION_PROVISION_MANAGED_PROFILE: - resId = R.array.required_apps_managed_profile; - break; - case ACTION_PROVISION_MANAGED_DEVICE: - resId = R.array.required_apps_managed_device; - break; - default: - throw new IllegalArgumentException("Provisioning type " - + provisioningAction + " not supported."); - } - return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId))); + final int resId = switch (provisioningAction) { + case ACTION_PROVISION_MANAGED_USER -> R.array.required_apps_managed_user; + case ACTION_PROVISION_MANAGED_PROFILE -> R.array.required_apps_managed_profile; + case ACTION_PROVISION_MANAGED_DEVICE -> R.array.required_apps_managed_device; + default -> throw new IllegalArgumentException( + "Provisioning type " + provisioningAction + " not supported."); + }; + return resolveStringArray(resId); } private Set<String> getDisallowedAppsSet(String provisioningAction) { - final int resId; - switch (provisioningAction) { - case ACTION_PROVISION_MANAGED_USER: - resId = R.array.disallowed_apps_managed_user; - break; - case ACTION_PROVISION_MANAGED_PROFILE: - resId = R.array.disallowed_apps_managed_profile; - break; - case ACTION_PROVISION_MANAGED_DEVICE: - resId = R.array.disallowed_apps_managed_device; - break; - default: - throw new IllegalArgumentException("Provisioning type " - + provisioningAction + " not supported."); - } - return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId))); + final int resId = switch (provisioningAction) { + case ACTION_PROVISION_MANAGED_USER -> R.array.disallowed_apps_managed_user; + case ACTION_PROVISION_MANAGED_PROFILE -> R.array.disallowed_apps_managed_profile; + case ACTION_PROVISION_MANAGED_DEVICE -> R.array.disallowed_apps_managed_device; + default -> throw new IllegalArgumentException( + "Provisioning type " + provisioningAction + " not supported."); + }; + return resolveStringArray(resId); } private Set<String> getVendorRequiredAppsSet(String provisioningAction) { - final int resId; - switch (provisioningAction) { - case ACTION_PROVISION_MANAGED_USER: - resId = R.array.vendor_required_apps_managed_user; - break; - case ACTION_PROVISION_MANAGED_PROFILE: - resId = R.array.vendor_required_apps_managed_profile; - break; - case ACTION_PROVISION_MANAGED_DEVICE: - resId = R.array.vendor_required_apps_managed_device; - break; - default: - throw new IllegalArgumentException("Provisioning type " - + provisioningAction + " not supported."); - } - return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId))); + final int resId = switch (provisioningAction) { + case ACTION_PROVISION_MANAGED_USER -> R.array.vendor_required_apps_managed_user; + case ACTION_PROVISION_MANAGED_PROFILE -> R.array.vendor_required_apps_managed_profile; + case ACTION_PROVISION_MANAGED_DEVICE -> R.array.vendor_required_apps_managed_device; + default -> throw new IllegalArgumentException( + "Provisioning type " + provisioningAction + " not supported."); + }; + return resolveStringArray(resId); } private Set<String> getVendorDisallowedAppsSet(String provisioningAction) { - final int resId; - switch (provisioningAction) { - case ACTION_PROVISION_MANAGED_USER: - resId = R.array.vendor_disallowed_apps_managed_user; - break; - case ACTION_PROVISION_MANAGED_PROFILE: - resId = R.array.vendor_disallowed_apps_managed_profile; - break; - case ACTION_PROVISION_MANAGED_DEVICE: - resId = R.array.vendor_disallowed_apps_managed_device; - break; - default: - throw new IllegalArgumentException("Provisioning type " - + provisioningAction + " not supported."); + final int resId = switch (provisioningAction) { + case ACTION_PROVISION_MANAGED_USER -> R.array.vendor_disallowed_apps_managed_user; + case ACTION_PROVISION_MANAGED_PROFILE -> R.array.vendor_disallowed_apps_managed_profile; + case ACTION_PROVISION_MANAGED_DEVICE -> R.array.vendor_disallowed_apps_managed_device; + default -> throw new IllegalArgumentException( + "Provisioning type " + provisioningAction + " not supported."); + }; + return resolveStringArray(resId); + } + + private Set<String> resolveStringArray(@ArrayRes int resId) { + if (Flags.isRecursiveRequiredAppMergingEnabled()) { + return mRecursiveStringArrayResourceResolver.resolve(mContext.getPackageName(), resId); + } else { + return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId))); } - return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId))); } void dump(IndentingPrintWriter pw) { pw.println("OverlayPackagesProvider"); pw.increaseIndent(); - dumpResources(pw, mContext, "required_apps_managed_device", - R.array.required_apps_managed_device); - dumpResources(pw, mContext, "required_apps_managed_user", - R.array.required_apps_managed_user); - dumpResources(pw, mContext, "required_apps_managed_profile", - R.array.required_apps_managed_profile); - - dumpResources(pw, mContext, "disallowed_apps_managed_device", - R.array.disallowed_apps_managed_device); - dumpResources(pw, mContext, "disallowed_apps_managed_user", - R.array.disallowed_apps_managed_user); - dumpResources(pw, mContext, "disallowed_apps_managed_device", - R.array.disallowed_apps_managed_device); - - dumpResources(pw, mContext, "vendor_required_apps_managed_device", - R.array.vendor_required_apps_managed_device); - dumpResources(pw, mContext, "vendor_required_apps_managed_user", - R.array.vendor_required_apps_managed_user); - dumpResources(pw, mContext, "vendor_required_apps_managed_profile", - R.array.vendor_required_apps_managed_profile); - - dumpResources(pw, mContext, "vendor_disallowed_apps_managed_user", - R.array.vendor_disallowed_apps_managed_user); - dumpResources(pw, mContext, "vendor_disallowed_apps_managed_device", - R.array.vendor_disallowed_apps_managed_device); - dumpResources(pw, mContext, "vendor_disallowed_apps_managed_profile", - R.array.vendor_disallowed_apps_managed_profile); + dumpApps(pw, "required_apps_managed_device", + resolveStringArray(R.array.required_apps_managed_device).toArray(String[]::new)); + dumpApps(pw, "required_apps_managed_user", + resolveStringArray(R.array.required_apps_managed_user).toArray(String[]::new)); + dumpApps(pw, "required_apps_managed_profile", + resolveStringArray(R.array.required_apps_managed_profile).toArray(String[]::new)); + + dumpApps(pw, "disallowed_apps_managed_device", + resolveStringArray(R.array.disallowed_apps_managed_device).toArray(String[]::new)); + dumpApps(pw, "disallowed_apps_managed_user", + resolveStringArray(R.array.disallowed_apps_managed_user).toArray(String[]::new)); + dumpApps(pw, "disallowed_apps_managed_device", + resolveStringArray(R.array.disallowed_apps_managed_device).toArray(String[]::new)); + + dumpApps(pw, "vendor_required_apps_managed_device", + resolveStringArray(R.array.vendor_required_apps_managed_device).toArray( + String[]::new)); + dumpApps(pw, "vendor_required_apps_managed_user", + resolveStringArray(R.array.vendor_required_apps_managed_user).toArray( + String[]::new)); + dumpApps(pw, "vendor_required_apps_managed_profile", + resolveStringArray(R.array.vendor_required_apps_managed_profile).toArray( + String[]::new)); + + dumpApps(pw, "vendor_disallowed_apps_managed_user", + resolveStringArray(R.array.vendor_disallowed_apps_managed_user).toArray( + String[]::new)); + dumpApps(pw, "vendor_disallowed_apps_managed_device", + resolveStringArray(R.array.vendor_disallowed_apps_managed_device).toArray( + String[]::new)); + dumpApps(pw, "vendor_disallowed_apps_managed_profile", + resolveStringArray(R.array.vendor_disallowed_apps_managed_profile).toArray( + String[]::new)); pw.decreaseIndent(); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java b/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java new file mode 100644 index 000000000000..935e051b64ea --- /dev/null +++ b/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.devicepolicy; + +import android.annotation.SuppressLint; +import android.content.res.Resources; + +import androidx.annotation.ArrayRes; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * A class encapsulating all the logic for recursive string-array resource resolution. + */ +public class RecursiveStringArrayResourceResolver { + private static final String IMPORT_PREFIX = "#import:"; + private static final String SEPARATOR = "/"; + private static final String PWP = "."; + + private final Resources mResources; + + /** + * @param resources Android resource access object to use when resolving resources + */ + public RecursiveStringArrayResourceResolver(Resources resources) { + this.mResources = resources; + } + + /** + * Resolves a given {@code <string-array/>} resource specified via + * {@param rootId} in {@param pkg}. During resolution all values prefixed with + * {@link #IMPORT_PREFIX} are expanded and injected + * into the final list at the position of the import statement, + * pushing all the following values (and their expansions) down. + * Circular imports are tracked and skipped to avoid infinite resolution loops without losing + * data. + * + * <p> + * The import statements are expected in a form of + * "{@link #IMPORT_PREFIX}{package}{@link #SEPARATOR}{resourceName}" + * If the resource being imported is from the same package, its package can be specified as a + * {@link #PWP} shorthand `.` + * > e.g.: + * > {@code "#import:com.android.internal/disallowed_apps_managed_user"} + * > {@code "#import:./disallowed_apps_managed_user"} + * + * <p> + * Any incorrect or unresolvable import statement + * will cause the entire resolution to fail with an error. + * + * @param pkg the package owning the resource + * @param rootId the id of the {@code <string-array>} resource within {@param pkg} to start the + * resolution from + * @return a flattened list of all the resolved string array values from the root resource + * as well as all the imported arrays + */ + public Set<String> resolve(String pkg, @ArrayRes int rootId) { + return resolve(List.of(), pkg, rootId); + } + + /** + * A version of resolve that tracks already imported resources + * to avoid circular imports and wasted work. + * + * @param cache a list of already resolved packages to be skipped for further resolution + */ + private Set<String> resolve(Collection<String> cache, String pkg, @ArrayRes int rootId) { + final var strings = mResources.getStringArray(rootId); + final var runningCache = new ArrayList<>(cache); + + final var result = new HashSet<String>(); + for (var string : strings) { + final String ref; + if (string.startsWith(IMPORT_PREFIX)) { + ref = string.substring(IMPORT_PREFIX.length()); + } else { + ref = null; + } + + if (ref == null) { + result.add(string); + } else if (!runningCache.contains(ref)) { + final var next = resolveImport(runningCache, pkg, ref); + runningCache.addAll(next); + result.addAll(next); + } + } + return result; + } + + /** + * Resolves an import of the {@code <string-array>} resource + * in the context of {@param importingPackage} by the provided {@param ref}. + * + * @param cache a list of already resolved packages to be passed along into chained + * {@link #resolve} calls + * @param importingPackage the package that owns the resource which defined the import being + * processed. + * It is also used to expand all {@link #PWP} shorthands in + * {@param ref} + * @param ref reference to the resource to be imported in a form of + * "{package}{@link #SEPARATOR}{resourceName}". + * e.g.: {@code com.android.internal/disallowed_apps_managed_user} + */ + private Set<String> resolveImport( + Collection<String> cache, + String importingPackage, + String ref) { + final var chunks = ref.split(SEPARATOR, 2); + final var pkg = chunks[0]; + final var name = chunks[1]; + final String resolvedPkg; + if (Objects.equals(pkg, PWP)) { + resolvedPkg = importingPackage; + } else { + resolvedPkg = pkg; + } + @SuppressLint("DiscouragedApi") final var importId = mResources.getIdentifier( + /* name = */ name, + /* defType = */ "array", + /* defPackage = */ resolvedPkg); + if (importId == 0) { + throw new Resources.NotFoundException( + /* name= */ String.format("%s:array/%s", resolvedPkg, name)); + } + return resolve(cache, resolvedPkg, importId); + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 3b2a3dd9763a..e202bbf022bc 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1230,10 +1230,6 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(ThermalManagerService.class); t.traceEnd(); - t.traceBegin("StartHintManager"); - mSystemServiceManager.startService(HintManagerService.class); - t.traceEnd(); - // Now that the power manager has been started, let the activity manager // initialize power management features. t.traceBegin("InitPowerManagement"); @@ -1614,6 +1610,10 @@ public final class SystemServer implements Dumpable { t.traceEnd(); } + t.traceBegin("StartHintManager"); + mSystemServiceManager.startService(HintManagerService.class); + t.traceEnd(); + // Grants default permissions and defines roles t.traceBegin("StartRoleManagerService"); LocalManagerRegistry.addManager(RoleServicePlatformHelper.class, diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java index b9c5b36f9775..b4cf79941c33 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -203,6 +203,7 @@ public class InputMethodManagerServiceTestBase { .thenReturn(new int[] {0}); when(mMockUserManagerInternal.getUserIds()).thenReturn(new int[] {0}); when(mMockActivityManagerInternal.isSystemReady()).thenReturn(true); + when(mMockActivityManagerInternal.getCurrentUserId()).thenReturn(mCallingUserId); when(mMockPackageManagerInternal.getPackageUid(anyString(), anyLong(), anyInt())) .thenReturn(Binder.getCallingUid()); when(mMockPackageManagerInternal.isSameApp(anyString(), anyLong(), anyInt(), anyInt())) diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java index cea65b55494d..9f46d0ba7df6 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java @@ -198,7 +198,9 @@ public class InputMethodManagerServiceWindowGainedFocusTest @Test public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException { - when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false); + // Run blockingly on ServiceThread to avoid that interfering with our stubbing. + mServiceThread.getThreadHandler().runWithScissors( + () -> when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false), 0); assertThat( startInputOrWindowGainedFocus( diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java index b0f7bfa33415..54de64e2f3a8 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java @@ -52,6 +52,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.display.brightness.clamper.BrightnessClamperController; import com.android.server.testutils.OffsettableClock; import org.junit.After; @@ -96,6 +97,8 @@ public class AutomaticBrightnessControllerTest { @Mock HysteresisLevels mScreenBrightnessThresholdsIdle; @Mock Handler mNoOpHandler; @Mock BrightnessRangeController mBrightnessRangeController; + @Mock + BrightnessClamperController mBrightnessClamperController; @Mock BrightnessThrottler mBrightnessThrottler; @Before @@ -161,7 +164,8 @@ public class AutomaticBrightnessControllerTest { mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle, mContext, mBrightnessRangeController, mBrightnessThrottler, useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1, - useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits + useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits, + mBrightnessClamperController ); when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn( diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index 35b69f812ff0..73a2f655da8d 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -44,6 +44,7 @@ import android.content.res.TypedArray; import android.hardware.display.DisplayManagerInternal; import android.os.PowerManager; import android.os.Temperature; +import android.platform.test.annotations.RequiresFlagsEnabled; import android.provider.Settings; import android.util.SparseArray; import android.util.Spline; @@ -57,6 +58,7 @@ import com.android.server.display.config.HdrBrightnessData; import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint; import com.android.server.display.config.ThermalStatus; import com.android.server.display.feature.DisplayManagerFlags; +import com.android.server.display.feature.flags.Flags; import org.junit.Before; import org.junit.Test; @@ -380,7 +382,7 @@ public final class DisplayDeviceConfigTest { public void testInvalidLuxThrottling() throws Exception { setupDisplayDeviceConfigFromDisplayConfigFile( getContent(getInvalidLuxThrottling(), getValidProxSensor(), - /* includeIdleMode= */ true)); + /* includeIdleMode= */ true, /* enableEvenDimmer */ false)); Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData = mDisplayDeviceConfig.getLuxThrottlingData(); @@ -588,7 +590,7 @@ public final class DisplayDeviceConfigTest { public void testProximitySensorWithEmptyValuesFromDisplayConfig() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile( getContent(getValidLuxThrottling(), getProxSensorWithEmptyValues(), - /* includeIdleMode= */ true)); + /* includeIdleMode= */ true, /* enableEvenDimmer */ false)); assertNull(mDisplayDeviceConfig.getProximitySensor()); } @@ -596,7 +598,7 @@ public final class DisplayDeviceConfigTest { public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile( getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(), - /* includeIdleMode= */ true)); + /* includeIdleMode= */ true, /* enableEvenDimmer */ false)); assertEquals("test_proximity_sensor", mDisplayDeviceConfig.getProximitySensor().type); assertEquals("Test Proximity Sensor", @@ -784,7 +786,7 @@ public final class DisplayDeviceConfigTest { @Test public void testBrightnessRamps_IdleFallsBackToConfigInteractive() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), - getValidProxSensor(), /* includeIdleMode= */ false)); + getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false)); assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000); assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000); @@ -801,14 +803,14 @@ public final class DisplayDeviceConfigTest { @Test public void testBrightnessCapForWearBedtimeMode() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), - getValidProxSensor(), /* includeIdleMode= */ false)); + getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false)); assertEquals(0.1f, mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA); } @Test public void testAutoBrightnessBrighteningLevels() throws IOException { setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), - getValidProxSensor(), /* includeIdleMode= */ false)); + getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false)); assertArrayEquals(new float[]{0.0f, 80}, mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux( @@ -871,7 +873,7 @@ public final class DisplayDeviceConfigTest { when(mFlags.areAutoBrightnessModesEnabled()).thenReturn(false); setupDisplayDeviceConfigFromConfigResourceFile(); setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), - getValidProxSensor(), /* includeIdleMode= */ false)); + getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false)); assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100), brightnessIntToFloat(150)}, @@ -904,6 +906,18 @@ public final class DisplayDeviceConfigTest { assertFalse(mDisplayDeviceConfig.isAutoBrightnessAvailable()); } + @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER) + @Test + public void testEvenDimmer() throws IOException { + when(mFlags.isEvenDimmerEnabled()).thenReturn(true); + setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(), + getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ true)); + + assertTrue(mDisplayDeviceConfig.getLbmEnabled()); + assertEquals(0.0001f, mDisplayDeviceConfig.getBacklightFromBrightness(0.1f), ZERO_DELTA); + assertEquals(0.2f, mDisplayDeviceConfig.getNitsFromBacklight(0.0f), ZERO_DELTA); + } + private String getValidLuxThrottling() { return "<luxThrottling>\n" + " <brightnessLimitMap>\n" @@ -1229,11 +1243,11 @@ public final class DisplayDeviceConfigTest { private String getContent() { return getContent(getValidLuxThrottling(), getValidProxSensor(), - /* includeIdleMode= */ true); + /* includeIdleMode= */ true, false); } private String getContent(String brightnessCapConfig, String proxSensor, - boolean includeIdleMode) { + boolean includeIdleMode, boolean enableEvenDimmer) { return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + "<displayConfiguration>\n" + "<name>Example Display</name>\n" @@ -1603,6 +1617,7 @@ public final class DisplayDeviceConfigTest { + "<majorVersion>2</majorVersion>\n" + "<minorVersion>0</minorVersion>\n" + "</usiVersion>\n" + + evenDimmerConfig(enableEvenDimmer) + "<screenBrightnessCapForWearBedtimeMode>" + "0.1" + "</screenBrightnessCapForWearBedtimeMode>" @@ -1621,6 +1636,24 @@ public final class DisplayDeviceConfigTest { + "</displayConfiguration>\n"; } + private String evenDimmerConfig(boolean enabled) { + return (enabled ? "<lowBrightness enabled=\"true\">" : "<lowBrightness enabled=\"false\">") + + " <transitionPoint>0.1</transitionPoint>\n" + + " <nits>0.2</nits>\n" + + " <nits>2.0</nits>\n" + + " <nits>500.0</nits>\n" + + " <nits>1000.0</nits>\n" + + " <backlight>0</backlight>\n" + + " <backlight>0.0001</backlight>\n" + + " <backlight>0.5</backlight>\n" + + " <backlight>1.0</backlight>\n" + + " <brightness>0</brightness>\n" + + " <brightness>0.1</brightness>\n" + + " <brightness>0.5</brightness>\n" + + " <brightness>1.0</brightness>\n" + + "</lowBrightness>"; + } + private void mockDeviceConfigs() { when(mResources.getFloat(com.android.internal.R.dimen .config_screenBrightnessSettingDefaultFloat)).thenReturn(0.5f); 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 01598aeba8fe..5842dacbd8f6 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -1184,7 +1184,8 @@ public final class DisplayPowerControllerTest { /* ambientLightHorizonShort= */ anyInt(), /* ambientLightHorizonLong= */ anyInt(), eq(lux), - eq(nits) + eq(nits), + any(BrightnessClamperController.class) ); } @@ -1546,6 +1547,47 @@ public final class DisplayPowerControllerTest { } @Test + public void testOffloadBlocker_turnON_screenOnBlocked() { + // set up. + int initState = Display.STATE_OFF; + mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); + mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession); + // start with OFF. + when(mHolder.displayPowerState.getScreenState()).thenReturn(initState); + DisplayPowerRequest dpr = new DisplayPowerRequest(); + dpr.policy = DisplayPowerRequest.POLICY_OFF; + mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); + advanceTime(1); // Run updatePowerState + + // go to ON. + dpr.policy = DisplayPowerRequest.POLICY_BRIGHT; + mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); + advanceTime(1); // Run updatePowerState + + verify(mDisplayOffloadSession).blockScreenOn(any(Runnable.class)); + } + + @Test + public void testOffloadBlocker_turnOFF_screenOnNotBlocked() { + // set up. + int initState = Display.STATE_ON; + mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession); + // start with ON. + when(mHolder.displayPowerState.getScreenState()).thenReturn(initState); + DisplayPowerRequest dpr = new DisplayPowerRequest(); + dpr.policy = DisplayPowerRequest.POLICY_BRIGHT; + mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); + advanceTime(1); // Run updatePowerState + + // go to OFF. + dpr.policy = DisplayPowerRequest.POLICY_OFF; + mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false); + advanceTime(1); // Run updatePowerState + + verify(mDisplayOffloadSession, never()).blockScreenOn(any(Runnable.class)); + } + + @Test public void testBrightnessFromOffload() { when(mDisplayManagerFlagsMock.isDisplayOffloadEnabled()).thenReturn(true); mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID); @@ -2121,7 +2163,8 @@ public final class DisplayPowerControllerTest { HysteresisLevels screenBrightnessThresholdsIdle, Context context, BrightnessRangeController brightnessRangeController, BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort, - int ambientLightHorizonLong, float userLux, float userNits) { + int ambientLightHorizonLong, float userLux, float userNits, + BrightnessClamperController brightnessClamperController) { return mAutomaticBrightnessController; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index 14de527aa1f7..7fd96c57c215 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -49,6 +49,7 @@ import android.os.Looper; import android.view.Display; import android.view.DisplayAddress; import android.view.SurfaceControl; +import android.view.SurfaceControl.IdleScreenRefreshRateConfig; import android.view.SurfaceControl.RefreshRateRange; import android.view.SurfaceControl.RefreshRateRanges; @@ -830,18 +831,20 @@ public class LocalDisplayAdapterTest { .get() .getModeId(); + IdleScreenRefreshRateConfig + idleScreenRefreshRateConfig = new SurfaceControl.IdleScreenRefreshRateConfig(500); displayDevice.setDesiredDisplayModeSpecsLocked( new DisplayModeDirector.DesiredDisplayModeSpecs( /*baseModeId*/ baseModeId, /*allowGroupSwitching*/ false, - REFRESH_RATE_RANGES, REFRESH_RATE_RANGES + REFRESH_RATE_RANGES, REFRESH_RATE_RANGES, idleScreenRefreshRateConfig )); waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); verify(mSurfaceControlProxy).setDesiredDisplayModeSpecs(display.token, new SurfaceControl.DesiredDisplayModeSpecs( /* baseModeId */ 0, /* allowGroupSwitching */ false, - REFRESH_RATE_RANGES, REFRESH_RATE_RANGES + REFRESH_RATE_RANGES, REFRESH_RATE_RANGES, idleScreenRefreshRateConfig )); // Change the display @@ -862,12 +865,13 @@ public class LocalDisplayAdapterTest { baseModeId = displayDevice.getDisplayDeviceInfoLocked().supportedModes[0].getModeId(); + idleScreenRefreshRateConfig = new SurfaceControl.IdleScreenRefreshRateConfig(600); // The traversal request will call setDesiredDisplayModeSpecsLocked on the display device displayDevice.setDesiredDisplayModeSpecsLocked( new DisplayModeDirector.DesiredDisplayModeSpecs( /*baseModeId*/ baseModeId, /*allowGroupSwitching*/ false, - REFRESH_RATE_RANGES, REFRESH_RATE_RANGES + REFRESH_RATE_RANGES, REFRESH_RATE_RANGES, idleScreenRefreshRateConfig )); waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); @@ -877,7 +881,7 @@ public class LocalDisplayAdapterTest { new SurfaceControl.DesiredDisplayModeSpecs( /* baseModeId */ 2, /* allowGroupSwitching */ false, - REFRESH_RATE_RANGES, REFRESH_RATE_RANGES + REFRESH_RATE_RANGES, REFRESH_RATE_RANGES, idleScreenRefreshRateConfig )); } @@ -1319,7 +1323,8 @@ public class LocalDisplayAdapterTest { new SurfaceControl.DesiredDisplayModeSpecs( /* defaultMode */ 0, /* allowGroupSwitching */ false, - REFRESH_RATE_RANGES, REFRESH_RATE_RANGES + REFRESH_RATE_RANGES, REFRESH_RATE_RANGES, + new IdleScreenRefreshRateConfig(100) ); private FakeDisplay(int port) { diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt index ac7d1f5ba452..e4a7d982514f 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt @@ -65,7 +65,7 @@ class BrightnessLowLuxModifierTest { Settings.Secure.putIntForUser(context.contentResolver, Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) Settings.Secure.putFloatForUser(context.contentResolver, - Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId) + Settings.Secure.EVEN_DIMMER_MIN_NITS, 30.0f, userId) modifier.recalculateLowerBound() testHandler.flush() assertThat(modifier.isActive).isTrue() @@ -81,11 +81,22 @@ class BrightnessLowLuxModifierTest { Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId) Settings.Secure.putFloatForUser(context.contentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.0f, userId) - modifier.recalculateLowerBound() + modifier.onAmbientLuxChange(3000.0f) testHandler.flush() assertThat(modifier.isActive).isTrue() // Test restriction from lux setting assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX) } + + @Test + fun testSettingOffDisablesModifier() { + Settings.Secure.putIntForUser(context.contentResolver, + Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, userId) + assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN) + modifier.onAmbientLuxChange(3000.0f) + testHandler.flush() + assertThat(modifier.isActive).isFalse() + assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN) + } } 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 3eced7fa025c..3a59c84b636c 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 @@ -75,6 +75,8 @@ import android.util.SparseArray; import android.util.TypedValue; import android.view.Display; import android.view.DisplayInfo; +import android.view.SurfaceControl; +import android.view.SurfaceControl.IdleScreenRefreshRateConfig; import android.view.SurfaceControl.RefreshRateRange; import android.view.SurfaceControl.RefreshRateRanges; @@ -91,6 +93,7 @@ import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.modules.utils.testing.ExtendedMockitoRule; import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.TestUtils; +import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.mode.DisplayModeDirector.BrightnessObserver; import com.android.server.display.mode.DisplayModeDirector.DesiredDisplayModeSpecs; @@ -112,6 +115,7 @@ import org.mockito.Mockito; import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -1405,6 +1409,81 @@ public class DisplayModeDirectorTest { } @Test + public void testIdleScreenTimeOnLuxChanges() throws Exception { + DisplayModeDirector director = + createDirectorFromRefreshRateArray(new float[] {60.f, 90.f, 120.f}, 0); + setPeakRefreshRate(120 /*fps*/); + director.getSettingsObserver().setDefaultRefreshRate(120); + director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON); + + // Set the DisplayDeviceConfig + DisplayDeviceConfig ddcMock = mock(DisplayDeviceConfig.class); + when(ddcMock.getDefaultHighBlockingZoneRefreshRate()).thenReturn(90); + when(ddcMock.getHighDisplayBrightnessThresholds()).thenReturn(new float[] { 200 }); + when(ddcMock.getHighAmbientBrightnessThresholds()).thenReturn(new float[] { 8000 }); + when(ddcMock.getDefaultLowBlockingZoneRefreshRate()).thenReturn(90); + when(ddcMock.getLowDisplayBrightnessThresholds()).thenReturn(new float[] {}); + when(ddcMock.getLowAmbientBrightnessThresholds()).thenReturn(new float[] {}); + + director.defaultDisplayDeviceUpdated(ddcMock); // set the ddc + + Sensor lightSensor = createLightSensor(); + SensorManager sensorManager = createMockSensorManager(lightSensor); + director.start(sensorManager); + + // Get the sensor listener so that we can give it new light sensor events + ArgumentCaptor<SensorEventListener> listenerCaptor = + ArgumentCaptor.forClass(SensorEventListener.class); + verify(sensorManager, Mockito.timeout(TimeUnit.SECONDS.toMillis(1))) + .registerListener( + listenerCaptor.capture(), + eq(lightSensor), + anyInt(), + any(Handler.class)); + SensorEventListener sensorListener = listenerCaptor.getValue(); + + // Disable the idle screen flag + when(mDisplayManagerFlags.isIdleScreenRefreshRateTimeoutEnabled()) + .thenReturn(false); + + // Sensor reads 5 lux, with idleScreenRefreshRate timeout not configured + sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 5)); + waitForIdleSync(); + assertEquals(null, director.getBrightnessObserver().getIdleScreenRefreshRateConfig()); + + // Enable the idle screen flag + when(mDisplayManagerFlags.isIdleScreenRefreshRateTimeoutEnabled()) + .thenReturn(true); + sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 8)); + waitForIdleSync(); + assertEquals(null, director.getBrightnessObserver().getIdleScreenRefreshRateConfig()); + + // Configure DDC with idle screen timeout + when(ddcMock.getIdleScreenRefreshRateTimeoutLuxThresholdPoint()) + .thenReturn(List.of(getIdleScreenRefreshRateTimeoutLuxThresholdPoint(6, 1000), + getIdleScreenRefreshRateTimeoutLuxThresholdPoint(100, 800))); + + // Sensor reads 5 lux + sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 5)); + waitForIdleSync(); + assertEquals(new SurfaceControl.IdleScreenRefreshRateConfig(-1), + director.getBrightnessObserver().getIdleScreenRefreshRateConfig()); + + // Sensor reads 50 lux + sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 50)); + waitForIdleSync(); + assertEquals(new IdleScreenRefreshRateConfig(1000), + director.getBrightnessObserver().getIdleScreenRefreshRateConfig()); + + // Sensor reads 200 lux + sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 200)); + waitForIdleSync(); + assertEquals(new SurfaceControl.IdleScreenRefreshRateConfig(800), + director.getBrightnessObserver().getIdleScreenRefreshRateConfig()); + + } + + @Test public void testLockFpsForHighZoneWithThermalCondition() throws Exception { // First, configure brightness zones or DMD won't register for sensor data. final FakeDeviceConfig config = mInjector.getDeviceConfig(); @@ -1440,11 +1519,11 @@ public class DisplayModeDirectorTest { // Get the display listener so that we can send it new brightness events ArgumentCaptor<DisplayListener> displayListenerCaptor = - ArgumentCaptor.forClass(DisplayListener.class); + ArgumentCaptor.forClass(DisplayListener.class); verify(mInjector).registerDisplayListener(displayListenerCaptor.capture(), any(Handler.class), eq(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED - | DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS)); + | DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS)); DisplayListener displayListener = displayListenerCaptor.getValue(); // Get the sensor listener so that we can give it new light sensor events @@ -3746,4 +3825,14 @@ public class DisplayModeDirectorTest { } } } + + private IdleScreenRefreshRateTimeoutLuxThresholdPoint + getIdleScreenRefreshRateTimeoutLuxThresholdPoint(int lux, int timeout) { + IdleScreenRefreshRateTimeoutLuxThresholdPoint + idleScreenRefreshRateTimeoutLuxThresholdPoint = + new IdleScreenRefreshRateTimeoutLuxThresholdPoint(); + idleScreenRefreshRateTimeoutLuxThresholdPoint.setLux(BigInteger.valueOf(lux)); + idleScreenRefreshRateTimeoutLuxThresholdPoint.setTimeout(BigInteger.valueOf(timeout)); + return idleScreenRefreshRateTimeoutLuxThresholdPoint; + } } diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index 6d3b8ac45913..4149e44a2ee9 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -75,6 +75,7 @@ android_test { "compatibility-device-util-axt", "flag-junit", "am_flags_lib", + "device_policy_aconfig_flags_lib", ], libs: [ diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java index c30ac2d6c248..682569f1d9ab 100644 --- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java @@ -26,6 +26,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.RescueParty.LEVEL_FACTORY_RESET; +import static com.android.server.RescueParty.RESCUE_LEVEL_FACTORY_RESET; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -41,9 +42,11 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; import android.os.RecoverySystem; import android.os.SystemProperties; import android.os.UserHandle; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.provider.Settings; import android.util.ArraySet; @@ -55,6 +58,7 @@ import com.android.server.am.SettingsToPropertiesMapper; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.Answers; import org.mockito.ArgumentCaptor; @@ -69,6 +73,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -100,6 +105,9 @@ public class RescuePartyTest { private static final int THROTTLING_DURATION_MIN = 10; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private MockitoSession mSession; private HashMap<String, String> mSystemSettingsMap; private HashMap<String, String> mCrashRecoveryPropertiesMap; @@ -267,6 +275,42 @@ public class RescuePartyTest { } @Test + public void testBootLoopDetectionWithExecutionForAllRescueLevelsRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescueParty.onSettingsProviderPublished(mMockContext); + verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), + any(Executor.class), + mMonitorCallbackCaptor.capture())); + HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>(); + + // Record DeviceConfig accesses + DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue(); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2); + + final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2}; + + noteBoot(1); + verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap); + + noteBoot(2); + assertTrue(RescueParty.isRebootPropertySet()); + + noteBoot(3); + verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS); + + noteBoot(4); + verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES); + + noteBoot(5); + verifyOnlySettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS); + + setCrashRecoveryPropAttemptingReboot(false); + noteBoot(6); + assertTrue(RescueParty.isFactoryResetPropertySet()); + } + + @Test public void testPersistentAppCrashDetectionWithExecutionForAllRescueLevels() { noteAppCrash(1, true); @@ -292,6 +336,47 @@ public class RescuePartyTest { } @Test + public void testPersistentAppCrashDetectionWithExecutionForAllRescueLevelsRecoverability() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescueParty.onSettingsProviderPublished(mMockContext); + verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), + any(Executor.class), + mMonitorCallbackCaptor.capture())); + HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>(); + + // Record DeviceConfig accesses + DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue(); + monitorCallback.onDeviceConfigAccess(PERSISTENT_PACKAGE, NAMESPACE1); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2); + + final String[] expectedResetNamespaces = new String[]{NAMESPACE1}; + final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2}; + + noteAppCrash(1, true); + verifyDeviceConfigReset(expectedResetNamespaces, verifiedTimesMap); + + noteAppCrash(2, true); + verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap); + + noteAppCrash(3, true); + assertTrue(RescueParty.isRebootPropertySet()); + + noteAppCrash(4, true); + verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS); + + noteAppCrash(5, true); + verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES); + + noteAppCrash(6, true); + verifyOnlySettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS); + + setCrashRecoveryPropAttemptingReboot(false); + noteAppCrash(7, true); + assertTrue(RescueParty.isFactoryResetPropertySet()); + } + + @Test public void testNonPersistentAppOnlyPerformsFlagResets() { noteAppCrash(1, false); @@ -316,6 +401,45 @@ public class RescuePartyTest { } @Test + public void testNonPersistentAppOnlyPerformsFlagResetsRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescueParty.onSettingsProviderPublished(mMockContext); + verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), + any(Executor.class), + mMonitorCallbackCaptor.capture())); + HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>(); + + // Record DeviceConfig accesses + DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue(); + monitorCallback.onDeviceConfigAccess(NON_PERSISTENT_PACKAGE, NAMESPACE1); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2); + + final String[] expectedResetNamespaces = new String[]{NAMESPACE1}; + final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2}; + + noteAppCrash(1, false); + verifyDeviceConfigReset(expectedResetNamespaces, verifiedTimesMap); + + noteAppCrash(2, false); + verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap); + + noteAppCrash(3, false); + assertFalse(RescueParty.isRebootPropertySet()); + + noteAppCrash(4, false); + verifyNoSettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS); + noteAppCrash(5, false); + verifyNoSettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES); + noteAppCrash(6, false); + verifyNoSettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS); + + setCrashRecoveryPropAttemptingReboot(false); + noteAppCrash(7, false); + assertFalse(RescueParty.isFactoryResetPropertySet()); + } + + @Test public void testNonPersistentAppCrashDetectionWithScopedResets() { RescueParty.onSettingsProviderPublished(mMockContext); verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), @@ -451,6 +575,19 @@ public class RescuePartyTest { } @Test + public void testIsRecoveryTriggeredRebootRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) { + noteBoot(i + 1); + } + assertFalse(RescueParty.isFactoryResetPropertySet()); + setCrashRecoveryPropAttemptingReboot(false); + noteBoot(RESCUE_LEVEL_FACTORY_RESET + 1); + assertTrue(RescueParty.isRecoveryTriggeredReboot()); + assertTrue(RescueParty.isFactoryResetPropertySet()); + } + + @Test public void testIsRecoveryTriggeredRebootOnlyAfterRebootCompleted() { for (int i = 0; i < LEVEL_FACTORY_RESET; i++) { noteBoot(i + 1); @@ -469,6 +606,25 @@ public class RescuePartyTest { } @Test + public void testIsRecoveryTriggeredRebootOnlyAfterRebootCompletedRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) { + noteBoot(i + 1); + } + int mitigationCount = RESCUE_LEVEL_FACTORY_RESET + 1; + assertFalse(RescueParty.isFactoryResetPropertySet()); + noteBoot(mitigationCount++); + assertFalse(RescueParty.isFactoryResetPropertySet()); + noteBoot(mitigationCount++); + assertFalse(RescueParty.isFactoryResetPropertySet()); + noteBoot(mitigationCount++); + setCrashRecoveryPropAttemptingReboot(false); + noteBoot(mitigationCount + 1); + assertTrue(RescueParty.isRecoveryTriggeredReboot()); + assertTrue(RescueParty.isFactoryResetPropertySet()); + } + + @Test public void testThrottlingOnBootFailures() { setCrashRecoveryPropAttemptingReboot(false); long now = System.currentTimeMillis(); @@ -481,6 +637,19 @@ public class RescuePartyTest { } @Test + public void testThrottlingOnBootFailuresRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + setCrashRecoveryPropAttemptingReboot(false); + long now = System.currentTimeMillis(); + long beforeTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN - 1); + setCrashRecoveryPropLastFactoryReset(beforeTimeout); + for (int i = 1; i <= RESCUE_LEVEL_FACTORY_RESET; i++) { + noteBoot(i); + } + assertFalse(RescueParty.isRecoveryTriggeredReboot()); + } + + @Test public void testThrottlingOnAppCrash() { setCrashRecoveryPropAttemptingReboot(false); long now = System.currentTimeMillis(); @@ -493,6 +662,19 @@ public class RescuePartyTest { } @Test + public void testThrottlingOnAppCrashRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + setCrashRecoveryPropAttemptingReboot(false); + long now = System.currentTimeMillis(); + long beforeTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN - 1); + setCrashRecoveryPropLastFactoryReset(beforeTimeout); + for (int i = 0; i <= RESCUE_LEVEL_FACTORY_RESET; i++) { + noteAppCrash(i + 1, true); + } + assertFalse(RescueParty.isRecoveryTriggeredReboot()); + } + + @Test public void testNotThrottlingAfterTimeoutOnBootFailures() { setCrashRecoveryPropAttemptingReboot(false); long now = System.currentTimeMillis(); @@ -503,6 +685,20 @@ public class RescuePartyTest { } assertTrue(RescueParty.isRecoveryTriggeredReboot()); } + + @Test + public void testNotThrottlingAfterTimeoutOnBootFailuresRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + setCrashRecoveryPropAttemptingReboot(false); + long now = System.currentTimeMillis(); + long afterTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN + 1); + setCrashRecoveryPropLastFactoryReset(afterTimeout); + for (int i = 1; i <= RESCUE_LEVEL_FACTORY_RESET; i++) { + noteBoot(i); + } + assertTrue(RescueParty.isRecoveryTriggeredReboot()); + } + @Test public void testNotThrottlingAfterTimeoutOnAppCrash() { setCrashRecoveryPropAttemptingReboot(false); @@ -516,6 +712,19 @@ public class RescuePartyTest { } @Test + public void testNotThrottlingAfterTimeoutOnAppCrashRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + setCrashRecoveryPropAttemptingReboot(false); + long now = System.currentTimeMillis(); + long afterTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN + 1); + setCrashRecoveryPropLastFactoryReset(afterTimeout); + for (int i = 0; i <= RESCUE_LEVEL_FACTORY_RESET; i++) { + noteAppCrash(i + 1, true); + } + assertTrue(RescueParty.isRecoveryTriggeredReboot()); + } + + @Test public void testNativeRescuePartyResets() { doReturn(true).when(() -> SettingsToPropertiesMapper.isNativeFlagsResetPerformed()); doReturn(FAKE_RESET_NATIVE_NAMESPACES).when( @@ -531,6 +740,7 @@ public class RescuePartyTest { @Test public void testExplicitlyEnablingAndDisablingRescue() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(false)); SystemProperties.set(PROP_DISABLE_RESCUE, Boolean.toString(true)); assertEquals(RescuePartyObserver.getInstance(mMockContext).execute(sFailingPackage, @@ -543,6 +753,7 @@ public class RescuePartyTest { @Test public void testDisablingRescueByDeviceConfigFlag() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(false)); SystemProperties.set(PROP_DEVICE_CONFIG_DISABLE_FLAG, Boolean.toString(true)); @@ -568,6 +779,20 @@ public class RescuePartyTest { } @Test + public void testDisablingFactoryResetByDeviceConfigFlagRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + SystemProperties.set(PROP_DISABLE_FACTORY_RESET_FLAG, Boolean.toString(true)); + + for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) { + noteBoot(i + 1); + } + assertFalse(RescueParty.isFactoryResetPropertySet()); + + // Restore the property value initialized in SetUp() + SystemProperties.set(PROP_DISABLE_FACTORY_RESET_FLAG, ""); + } + + @Test public void testHealthCheckLevels() { RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); @@ -594,6 +819,46 @@ public class RescuePartyTest { } @Test + public void testHealthCheckLevelsRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); + + // Ensure that no action is taken for cases where the failure reason is unknown + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_UNKNOWN, 1), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_0); + + // Ensure the correct user impact is returned for each mitigation count. + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 1), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 2), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 3), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 4), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 5), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 6), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 7), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + } + + @Test public void testBootLoopLevels() { RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); @@ -606,6 +871,19 @@ public class RescuePartyTest { } @Test + public void testBootLoopLevelsRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); + + assertEquals(observer.onBootLoop(1), PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + assertEquals(observer.onBootLoop(2), PackageHealthObserverImpact.USER_IMPACT_LEVEL_50); + assertEquals(observer.onBootLoop(3), PackageHealthObserverImpact.USER_IMPACT_LEVEL_71); + assertEquals(observer.onBootLoop(4), PackageHealthObserverImpact.USER_IMPACT_LEVEL_75); + assertEquals(observer.onBootLoop(5), PackageHealthObserverImpact.USER_IMPACT_LEVEL_80); + assertEquals(observer.onBootLoop(6), PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); + } + + @Test public void testResetDeviceConfigForPackagesOnlyRuntimeMap() { RescueParty.onSettingsProviderPublished(mMockContext); verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), @@ -727,11 +1005,26 @@ public class RescuePartyTest { private void verifySettingsResets(int resetMode, String[] resetNamespaces, HashMap<String, Integer> configResetVerifiedTimesMap) { + verifyOnlySettingsReset(resetMode); + verifyDeviceConfigReset(resetNamespaces, configResetVerifiedTimesMap); + } + + private void verifyOnlySettingsReset(int resetMode) { verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null, resetMode, UserHandle.USER_SYSTEM)); verify(() -> Settings.Secure.resetToDefaultsAsUser(eq(mMockContentResolver), isNull(), eq(resetMode), anyInt())); - // Verify DeviceConfig resets + } + + private void verifyNoSettingsReset(int resetMode) { + verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null, + resetMode, UserHandle.USER_SYSTEM), never()); + verify(() -> Settings.Secure.resetToDefaultsAsUser(eq(mMockContentResolver), isNull(), + eq(resetMode), anyInt()), never()); + } + + private void verifyDeviceConfigReset(String[] resetNamespaces, + Map<String, Integer> configResetVerifiedTimesMap) { if (resetNamespaces == null) { verify(() -> DeviceConfig.resetToDefaults(anyInt(), anyString()), never()); } else { @@ -818,9 +1111,16 @@ public class RescuePartyTest { // mock properties in BootThreshold try { - mSpyBootThreshold = spy(watchdog.new BootThreshold( - PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, - PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); + if (Flags.recoverabilityDetection()) { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, + PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + } else { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); + } mCrashRecoveryPropertiesMap = new HashMap<>(); doAnswer((Answer<Integer>) invocationOnMock -> { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 420af86c4408..1b2c0e4949e2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -41,6 +41,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; @@ -57,6 +58,7 @@ import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityManager; import android.app.AppOpsManager; +import android.app.ApplicationExitInfo; import android.app.BackgroundStartPrivileges; import android.app.BroadcastOptions; import android.app.IApplicationThread; @@ -239,6 +241,7 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { mConstants.TIMEOUT = 200; mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0; mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500; + mConstants.MAX_FROZEN_OUTGOING_BROADCASTS = 10; } @After @@ -2368,6 +2371,34 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { verifyScheduleReceiver(times(1), receiverYellowApp, timeTick); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_DEFER_OUTGOING_BROADCASTS) + public void testKillProcess_excessiveOutgoingBroadcastsWhileCached() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + setProcessFreezable(callerApp, true /* pendingFreeze */, false /* frozen */); + waitForIdle(); + + final int count = mConstants.MAX_FROZEN_OUTGOING_BROADCASTS + 1; + for (int i = 0; i < count; ++i) { + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK + "_" + i); + enqueueBroadcast(makeBroadcastRecord(timeTick, callerApp, List.of( + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)))); + } + // Verify that we invoke the call to freeze the caller app. + verify(mAms.mOomAdjuster.mCachedAppOptimizer, atLeastOnce()) + .freezeAppAsyncImmediateLSP(callerApp); + + // Verify that the caller process is killed + assertTrue(callerApp.isKilled()); + verify(mProcessList).noteAppKill(same(callerApp), + eq(ApplicationExitInfo.REASON_OTHER), + eq(ApplicationExitInfo.SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED), + any(String.class)); + + waitForIdle(); + assertNull(mAms.getProcessRecordLocked(PACKAGE_BLUE, getUidForPackage(PACKAGE_BLUE))); + } + private long getReceiverScheduledTime(@NonNull BroadcastRecord r, @NonNull Object receiver) { for (int i = 0; i < r.receivers.size(); ++i) { if (isReceiverEquals(receiver, r.receivers.get(i))) { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java index 97b7af8e43ad..680ab1634cb2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java @@ -36,7 +36,6 @@ import static com.android.server.am.ProcessList.SERVICE_ADJ; import static org.junit.Assert.assertNotEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; @@ -185,8 +184,8 @@ public final class ServiceBindingOomAdjPolicyTest { doReturn(false).when(mAms.mAtmInternal).hasSystemAlertWindowPermission(anyInt(), anyInt(), any()); doReturn(true).when(mAms.mOomAdjuster.mCachedAppOptimizer).useFreezer(); - doNothing().when(mAms.mOomAdjuster.mCachedAppOptimizer).freezeAppAsyncInternalLSP( - any(), anyLong(), anyBoolean(), anyBoolean()); + doNothing().when(mAms.mOomAdjuster.mCachedAppOptimizer).freezeAppAsyncAtEarliestLSP( + any()); doReturn(false).when(mAms.mAppProfiler).updateLowMemStateLSP(anyInt(), anyInt(), anyInt(), anyLong()); @@ -503,7 +502,7 @@ public final class ServiceBindingOomAdjPolicyTest { if (clientApp.isFreezable()) { verify(mAms.mOomAdjuster.mCachedAppOptimizer, times(Flags.serviceBindingOomAdjPolicy() ? 1 : 0)) - .freezeAppAsyncInternalLSP(eq(clientApp), eq(0L), anyBoolean(), anyBoolean()); + .freezeAppAsyncAtEarliestLSP(eq(clientApp)); clearInvocations(mAms.mOomAdjuster.mCachedAppOptimizer); } diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/PackageManagerBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/PackageManagerBackupAgentTest.java index 20e198c192bf..822dacf0404e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/PackageManagerBackupAgentTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/PackageManagerBackupAgentTest.java @@ -16,14 +16,19 @@ package com.android.server.backup; -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; - import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + import android.app.backup.BackupDataInput; import android.app.backup.BackupDataOutput; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.Signature; +import android.content.pm.SigningDetails; +import android.content.pm.SigningInfo; import android.os.Build; import android.os.ParcelFileDescriptor; import android.platform.test.annotations.Presubmit; @@ -38,6 +43,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import java.io.BufferedOutputStream; import java.io.DataOutputStream; @@ -51,22 +58,30 @@ import java.util.Optional; public class PackageManagerBackupAgentTest { private static final String EXISTING_PACKAGE_NAME = "com.android.wallpaperbackup"; + private static final int EXISTING_PACKAGE_VERSION = 1; + private static final int USER_ID = 0; @Rule public TemporaryFolder folder = new TemporaryFolder(); - private PackageManager mPackageManager; + @Mock private PackageManager mPackageManager; + private PackageManagerBackupAgent mPackageManagerBackupAgent; private ImmutableList<PackageInfo> mPackages; private File mBackupData, mOldState, mNewState; @Before public void setUp() throws Exception { - mPackageManager = getApplicationContext().getPackageManager(); + MockitoAnnotations.initMocks(this); PackageInfo existingPackageInfo = - mPackageManager.getPackageInfoAsUser( - EXISTING_PACKAGE_NAME, PackageManager.GET_SIGNING_CERTIFICATES, USER_ID); + createPackage(EXISTING_PACKAGE_NAME, EXISTING_PACKAGE_VERSION); + Signature sig = new Signature(new byte[256]); + existingPackageInfo.signingInfo = + new SigningInfo(new SigningDetails(new Signature[] {sig}, 1, null, null)); + when(mPackageManager.getPackageInfoAsUser(eq(EXISTING_PACKAGE_NAME), anyInt(), anyInt())) + .thenReturn(existingPackageInfo); + mPackages = ImmutableList.of(existingPackageInfo); mPackageManagerBackupAgent = new PackageManagerBackupAgent(mPackageManager, mPackages, USER_ID); diff --git a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java index ca730910943b..7f1a0bb1a5da 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/injector/TestInjector.java @@ -110,7 +110,7 @@ public class TestInjector implements Injector { } @Override - public EmergencyHelper getEmergencyHelper() { + public FakeEmergencyHelper getEmergencyHelper() { return mEmergencyHelper; } diff --git a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java index 32878b3e199f..09282646ff68 100644 --- a/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/location/provider/LocationProviderManagerTest.java @@ -16,6 +16,9 @@ package com.android.server.location.provider; +import static android.Manifest.permission.ACCESS_COARSE_LOCATION; +import static android.Manifest.permission.ACCESS_FINE_LOCATION; +import static android.Manifest.permission.LOCATION_BYPASS; import static android.app.AppOpsManager.OP_FINE_LOCATION; import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION; import static android.app.AppOpsManager.OP_MONITOR_LOCATION; @@ -1170,6 +1173,63 @@ public class LocationProviderManagerTest { } @Test + public void testProviderRequest_IgnoreLocationSettings_LocationBypass() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LOCATION_BYPASS); + + doReturn(PackageManager.PERMISSION_GRANTED) + .when(mContext) + .checkPermission(LOCATION_BYPASS, IDENTITY.getPid(), IDENTITY.getUid()); + mInjector.getLocationPermissionsHelper() + .revokePermission(IDENTITY.getPackageName(), ACCESS_FINE_LOCATION); + mInjector.getLocationPermissionsHelper() + .revokePermission(IDENTITY.getPackageName(), ACCESS_COARSE_LOCATION); + mInjector + .getSettingsHelper() + .setIgnoreSettingsAllowlist( + new PackageTagsList.Builder().add(IDENTITY.getPackageName()).build()); + + ILocationListener listener = createMockLocationListener(); + LocationRequest request = + new LocationRequest.Builder(1) + .setLocationSettingsIgnored(true) + .setWorkSource(WORK_SOURCE) + .build(); + mManager.registerLocationRequest(request, IDENTITY, PERMISSION_FINE, listener); + + assertThat(mProvider.getRequest().isActive()).isFalse(); + } + + @Test + public void testProviderRequest_IgnoreLocationSettings_LocationBypass_EmergencyCall() { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LOCATION_BYPASS); + + doReturn(PackageManager.PERMISSION_GRANTED) + .when(mContext) + .checkPermission(LOCATION_BYPASS, IDENTITY.getPid(), IDENTITY.getUid()); + mInjector.getLocationPermissionsHelper() + .revokePermission(IDENTITY.getPackageName(), ACCESS_FINE_LOCATION); + mInjector.getLocationPermissionsHelper() + .revokePermission(IDENTITY.getPackageName(), ACCESS_COARSE_LOCATION); + mInjector.getEmergencyHelper().setInEmergency(true); + mInjector + .getSettingsHelper() + .setIgnoreSettingsAllowlist( + new PackageTagsList.Builder().add(IDENTITY.getPackageName()).build()); + + ILocationListener listener = createMockLocationListener(); + LocationRequest request = + new LocationRequest.Builder(1) + .setLocationSettingsIgnored(true) + .setWorkSource(WORK_SOURCE) + .build(); + mManager.registerLocationRequest(request, IDENTITY, PERMISSION_FINE, listener); + + assertThat(mProvider.getRequest().isActive()).isTrue(); + assertThat(mProvider.getRequest().getIntervalMillis()).isEqualTo(1); + assertThat(mProvider.getRequest().isLocationSettingsIgnored()).isTrue(); + } + + @Test public void testProviderRequest_BackgroundThrottle_IgnoreLocationSettings() { mInjector.getSettingsHelper().setIgnoreSettingsAllowlist( new PackageTagsList.Builder().add( diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java deleted file mode 100644 index 9a7ee4d7887b..000000000000 --- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java +++ /dev/null @@ -1,684 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.pm; - -import static com.android.server.pm.BackgroundDexOptService.STATUS_DEX_OPT_FAILED; -import static com.android.server.pm.BackgroundDexOptService.STATUS_FATAL_ERROR; -import static com.android.server.pm.BackgroundDexOptService.STATUS_OK; - -import static com.google.common.truth.Truth.assertThat; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.timeout; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.testng.Assert.assertThrows; - -import android.annotation.Nullable; -import android.app.job.JobInfo; -import android.app.job.JobParameters; -import android.app.job.JobScheduler; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.HandlerThread; -import android.os.PowerManager; -import android.os.Process; -import android.os.SystemProperties; -import android.util.Log; - -import com.android.internal.util.IndentingPrintWriter; -import com.android.server.LocalServices; -import com.android.server.PinnerService; -import com.android.server.pm.dex.DexManager; -import com.android.server.pm.dex.DexoptOptions; - -import org.junit.After; -import org.junit.Assume; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InOrder; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import java.io.ByteArrayOutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.stream.Collectors; - -@RunWith(MockitoJUnitRunner.class) -public final class BackgroundDexOptServiceUnitTest { - private static final String TAG = BackgroundDexOptServiceUnitTest.class.getSimpleName(); - - private static final long USABLE_SPACE_NORMAL = 1_000_000_000; - private static final long STORAGE_LOW_BYTES = 1_000_000; - - private static final long TEST_WAIT_TIMEOUT_MS = 10_000; - - private static final String PACKAGE_AAA = "aaa"; - private static final List<String> DEFAULT_PACKAGE_LIST = List.of(PACKAGE_AAA, "bbb"); - private int mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED; - - // Store expected dexopt sequence for verification. - private ArrayList<DexOptInfo> mDexInfoSequence = new ArrayList<>(); - - @Mock - private Context mContext; - @Mock - private PackageManagerService mPackageManager; - @Mock - private DexOptHelper mDexOptHelper; - @Mock - private DexManager mDexManager; - @Mock - private PinnerService mPinnerService; - @Mock - private JobScheduler mJobScheduler; - @Mock - private BackgroundDexOptService.Injector mInjector; - @Mock - private BackgroundDexOptJobService mJobServiceForPostBoot; - @Mock - private BackgroundDexOptJobService mJobServiceForIdle; - - private final JobParameters mJobParametersForPostBoot = - createJobParameters(BackgroundDexOptService.JOB_POST_BOOT_UPDATE); - private final JobParameters mJobParametersForIdle = - createJobParameters(BackgroundDexOptService.JOB_IDLE_OPTIMIZE); - - private static JobParameters createJobParameters(int jobId) { - JobParameters params = mock(JobParameters.class); - when(params.getJobId()).thenReturn(jobId); - return params; - } - - private BackgroundDexOptService mService; - - private StartAndWaitThread mDexOptThread; - private StartAndWaitThread mCancelThread; - - @Before - public void setUp() throws Exception { - // These tests are only applicable to the legacy BackgroundDexOptService and cannot be run - // when ART Service is enabled. - Assume.assumeFalse(SystemProperties.getBoolean("dalvik.vm.useartservice", false)); - - when(mInjector.getCallingUid()).thenReturn(Process.FIRST_APPLICATION_UID); - when(mInjector.getContext()).thenReturn(mContext); - when(mInjector.getDexOptHelper()).thenReturn(mDexOptHelper); - when(mInjector.getDexManager()).thenReturn(mDexManager); - when(mInjector.getPinnerService()).thenReturn(mPinnerService); - when(mInjector.getJobScheduler()).thenReturn(mJobScheduler); - when(mInjector.getPackageManagerService()).thenReturn(mPackageManager); - - // These mocking can be overwritten in some tests but still keep it here as alternative - // takes too many repetitive codes. - when(mInjector.getDataDirUsableSpace()).thenReturn(USABLE_SPACE_NORMAL); - when(mInjector.getDataDirStorageLowBytes()).thenReturn(STORAGE_LOW_BYTES); - when(mInjector.getDexOptThermalCutoff()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL); - when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_NONE); - when(mInjector.supportSecondaryDex()).thenReturn(true); - setupDexOptHelper(); - - mService = new BackgroundDexOptService(mInjector); - } - - private void setupDexOptHelper() { - when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(DEFAULT_PACKAGE_LIST); - when(mDexOptHelper.performDexOptWithStatus(any())).thenAnswer(inv -> { - DexoptOptions opt = inv.getArgument(0); - if (opt.getPackageName().equals(PACKAGE_AAA)) { - return mDexOptResultForPackageAAA; - } - return PackageDexOptimizer.DEX_OPT_PERFORMED; - }); - when(mDexOptHelper.performDexOpt(any())).thenReturn(true); - } - - @After - public void tearDown() throws Exception { - LocalServices.removeServiceForTest(BackgroundDexOptService.class); - } - - @Test - public void testGetService() { - assertThat(BackgroundDexOptService.getService()).isEqualTo(mService); - } - - @Test - public void testBootCompleted() throws Exception { - initUntilBootCompleted(); - } - - @Test - public void testNoExecutionForIdleJobBeforePostBootUpdate() throws Exception { - initUntilBootCompleted(); - - assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isFalse(); - } - - @Test - public void testNoExecutionForLowStorage() throws Exception { - initUntilBootCompleted(); - when(mPackageManager.isStorageLow()).thenReturn(true); - - assertThat(mService.onStartJob(mJobServiceForPostBoot, - mJobParametersForPostBoot)).isFalse(); - verify(mDexOptHelper, never()).performDexOpt(any()); - } - - @Test - public void testNoExecutionForNoOptimizablePackages() throws Exception { - initUntilBootCompleted(); - when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(Collections.emptyList()); - - assertThat(mService.onStartJob(mJobServiceForPostBoot, - mJobParametersForPostBoot)).isFalse(); - verify(mDexOptHelper, never()).performDexOpt(any()); - } - - @Test - public void testPostBootUpdateFullRun() throws Exception { - initUntilBootCompleted(); - - runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, - /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, - /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); - } - - @Test - public void testPostBootUpdateFullRunWithPackageFailure() throws Exception { - mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED; - - initUntilBootCompleted(); - - runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, - /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED, - /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); - - assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); - assertThat(getFailedPackageNamesSecondary()).isEmpty(); - } - - @Test - public void testIdleJobFullRun() throws Exception { - initUntilBootCompleted(); - runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, - /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, - /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); - runFullJob(mJobServiceForIdle, mJobParametersForIdle, - /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, - /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); - } - - @Test - public void testIdleJobFullRunWithFailureOnceAndSuccessAfterUpdate() throws Exception { - mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED; - - initUntilBootCompleted(); - - runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, - /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED, - /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); - - assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); - assertThat(getFailedPackageNamesSecondary()).isEmpty(); - - runFullJob(mJobServiceForIdle, mJobParametersForIdle, - /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, - /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); - - assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); - assertThat(getFailedPackageNamesSecondary()).isEmpty(); - - mService.notifyPackageChanged(PACKAGE_AAA); - - assertThat(getFailedPackageNamesPrimary()).isEmpty(); - assertThat(getFailedPackageNamesSecondary()).isEmpty(); - - // Succeed this time. - mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED; - - runFullJob(mJobServiceForIdle, mJobParametersForIdle, - /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, - /* totalJobFinishedWithParams= */ 2, /* expectedSkippedPackage= */ null); - - assertThat(getFailedPackageNamesPrimary()).isEmpty(); - assertThat(getFailedPackageNamesSecondary()).isEmpty(); - } - - @Test - public void testIdleJobFullRunWithFatalError() throws Exception { - initUntilBootCompleted(); - runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, - /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, - /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); - - doThrow(RuntimeException.class).when(mDexOptHelper).performDexOptWithStatus(any()); - - runFullJob(mJobServiceForIdle, mJobParametersForIdle, - /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_FATAL_ERROR, - /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); - } - - @Test - public void testSystemReadyWhenDisabled() throws Exception { - when(mInjector.isBackgroundDexOptDisabled()).thenReturn(true); - - mService.systemReady(); - - verify(mContext, never()).registerReceiver(any(), any()); - } - - @Test - public void testStopByCancelFlag() throws Exception { - when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread()); - initUntilBootCompleted(); - - assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); - - ArgumentCaptor<Runnable> argDexOptThreadRunnable = ArgumentCaptor.forClass(Runnable.class); - verify(mInjector, atLeastOnce()).createAndStartThread(any(), - argDexOptThreadRunnable.capture()); - - // Stopping requires a separate thread - HandlerThread cancelThread = new HandlerThread("Stopping"); - cancelThread.start(); - when(mInjector.createAndStartThread(any(), any())).thenReturn(cancelThread); - - // Cancel - assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); - - // Capture Runnable for cancel - ArgumentCaptor<Runnable> argCancelThreadRunnable = ArgumentCaptor.forClass(Runnable.class); - verify(mInjector, atLeastOnce()).createAndStartThread(any(), - argCancelThreadRunnable.capture()); - - // Execute cancelling part - cancelThread.getThreadHandler().post(argCancelThreadRunnable.getValue()); - - verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking(true); - - // Dexopt thread run and cancelled - argDexOptThreadRunnable.getValue().run(); - - // Wait until cancellation Runnable is completed. - assertThat(cancelThread.getThreadHandler().runWithScissors( - argCancelThreadRunnable.getValue(), TEST_WAIT_TIMEOUT_MS)).isTrue(); - - // Now cancel completed - verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true); - verifyLastControlDexOptBlockingCall(false); - } - - @Test - public void testPostUpdateCancelFirst() throws Exception { - initUntilBootCompleted(); - when(mInjector.createAndStartThread(any(), any())).thenAnswer( - i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1))); - - // Start - assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); - // Cancel - assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); - - mCancelThread.runActualRunnable(); - - // Wait until cancel has set the flag. - verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking( - true); - - mDexOptThread.runActualRunnable(); - - // All threads should finish. - mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); - mCancelThread.join(TEST_WAIT_TIMEOUT_MS); - - // Retry later if post boot job was cancelled - verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true); - verifyLastControlDexOptBlockingCall(false); - } - - @Test - public void testPostUpdateCancelLater() throws Exception { - initUntilBootCompleted(); - when(mInjector.createAndStartThread(any(), any())).thenAnswer( - i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1))); - - // Start - assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); - // Cancel - assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); - - // Dexopt thread runs and finishes - mDexOptThread.runActualRunnable(); - mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); - - mCancelThread.runActualRunnable(); - mCancelThread.join(TEST_WAIT_TIMEOUT_MS); - - // Already completed before cancel, so no rescheduling. - verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, false); - verify(mDexOptHelper, never()).controlDexOptBlocking(true); - } - - @Test - public void testPeriodicJobCancelFirst() throws Exception { - initUntilBootCompleted(); - when(mInjector.createAndStartThread(any(), any())).thenAnswer( - i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1))); - - // Start and finish post boot job - assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); - mDexOptThread.runActualRunnable(); - mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); - - // Start - assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue(); - // Cancel - assertThat(mService.onStopJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue(); - - mCancelThread.runActualRunnable(); - - // Wait until cancel has set the flag. - verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking( - true); - - mDexOptThread.runActualRunnable(); - - // All threads should finish. - mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); - mCancelThread.join(TEST_WAIT_TIMEOUT_MS); - - // The job should be rescheduled. - verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true /* wantsReschedule */); - verifyLastControlDexOptBlockingCall(false); - } - - @Test - public void testPeriodicJobCancelLater() throws Exception { - initUntilBootCompleted(); - when(mInjector.createAndStartThread(any(), any())).thenAnswer( - i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1))); - - // Start and finish post boot job - assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); - mDexOptThread.runActualRunnable(); - mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); - - // Start - assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue(); - // Cancel - assertThat(mService.onStopJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue(); - - // Dexopt thread finishes first. - mDexOptThread.runActualRunnable(); - mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); - - mCancelThread.runActualRunnable(); - mCancelThread.join(TEST_WAIT_TIMEOUT_MS); - - // Always reschedule for periodic job - verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, false); - verify(mDexOptHelper, never()).controlDexOptBlocking(true); - } - - @Test - public void testStopByThermal() throws Exception { - when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread()); - initUntilBootCompleted(); - - assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); - - ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class); - verify(mInjector, atLeastOnce()).createAndStartThread(any(), argThreadRunnable.capture()); - - // Thermal cancel level - when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL); - - argThreadRunnable.getValue().run(); - - verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true); - verifyLastControlDexOptBlockingCall(false); - } - - @Test - public void testRunShellCommandWithInvalidUid() { - // Test uid cannot execute the command APIs - assertThrows(SecurityException.class, () -> mService.runBackgroundDexoptJob(null)); - } - - @Test - public void testCancelShellCommandWithInvalidUid() { - // Test uid cannot execute the command APIs - assertThrows(SecurityException.class, () -> mService.cancelBackgroundDexoptJob()); - } - - @Test - public void testDisableJobSchedulerJobs() throws Exception { - when(mInjector.getCallingUid()).thenReturn(Process.SHELL_UID); - mService.setDisableJobSchedulerJobs(true); - assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isFalse(); - verify(mDexOptHelper, never()).performDexOpt(any()); - verify(mDexOptHelper, never()).performDexOptWithStatus(any()); - } - - @Test - public void testSetDisableJobSchedulerJobsWithInvalidUid() { - // Test uid cannot execute the command APIs - assertThrows(SecurityException.class, () -> mService.setDisableJobSchedulerJobs(true)); - } - - private void initUntilBootCompleted() throws Exception { - ArgumentCaptor<BroadcastReceiver> argReceiver = ArgumentCaptor.forClass( - BroadcastReceiver.class); - ArgumentCaptor<IntentFilter> argIntentFilter = ArgumentCaptor.forClass(IntentFilter.class); - - mService.systemReady(); - - verify(mContext).registerReceiver(argReceiver.capture(), argIntentFilter.capture()); - assertThat(argIntentFilter.getValue().getAction(0)).isEqualTo(Intent.ACTION_BOOT_COMPLETED); - - argReceiver.getValue().onReceive(mContext, null); - - verify(mContext).unregisterReceiver(argReceiver.getValue()); - ArgumentCaptor<JobInfo> argJobs = ArgumentCaptor.forClass(JobInfo.class); - verify(mJobScheduler, times(2)).schedule(argJobs.capture()); - - List<Integer> expectedJobIds = Arrays.asList(BackgroundDexOptService.JOB_IDLE_OPTIMIZE, - BackgroundDexOptService.JOB_POST_BOOT_UPDATE); - List<Integer> jobIds = argJobs.getAllValues().stream().map(job -> job.getId()).collect( - Collectors.toList()); - assertThat(jobIds).containsExactlyElementsIn(expectedJobIds); - } - - private void verifyLastControlDexOptBlockingCall(boolean expected) throws Exception { - ArgumentCaptor<Boolean> argDexOptBlock = ArgumentCaptor.forClass(Boolean.class); - verify(mDexOptHelper, atLeastOnce()).controlDexOptBlocking(argDexOptBlock.capture()); - assertThat(argDexOptBlock.getValue()).isEqualTo(expected); - } - - private void runFullJob(BackgroundDexOptJobService jobService, JobParameters params, - boolean expectedReschedule, int expectedStatus, int totalJobFinishedWithParams, - @Nullable String expectedSkippedPackage) throws Exception { - when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread()); - addFullRunSequence(expectedSkippedPackage); - assertThat(mService.onStartJob(jobService, params)).isTrue(); - - ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class); - verify(mInjector, atLeastOnce()).createAndStartThread(any(), argThreadRunnable.capture()); - - try { - argThreadRunnable.getValue().run(); - } catch (RuntimeException e) { - if (expectedStatus != STATUS_FATAL_ERROR) { - throw e; - } - } - - verify(jobService, times(totalJobFinishedWithParams)).jobFinished(params, - expectedReschedule); - // Never block - verify(mDexOptHelper, never()).controlDexOptBlocking(true); - if (expectedStatus != STATUS_FATAL_ERROR) { - verifyPerformDexOpt(); - } - assertThat(getLastExecutionStatus()).isEqualTo(expectedStatus); - } - - private void verifyPerformDexOpt() { - InOrder inOrder = inOrder(mDexOptHelper); - inOrder.verify(mDexOptHelper).getOptimizablePackages(any()); - for (DexOptInfo info : mDexInfoSequence) { - if (info.isPrimary) { - verify(mDexOptHelper).performDexOptWithStatus( - argThat((option) -> option.getPackageName().equals(info.packageName) - && !option.isDexoptOnlySecondaryDex())); - } else { - inOrder.verify(mDexOptHelper).performDexOpt( - argThat((option) -> option.getPackageName().equals(info.packageName) - && option.isDexoptOnlySecondaryDex())); - } - } - - // Even InOrder cannot check the order if the same call is made multiple times. - // To check the order across multiple runs, we reset the mock so that order can be checked - // in each call. - mDexInfoSequence.clear(); - reset(mDexOptHelper); - setupDexOptHelper(); - } - - private String findDumpValueForKey(String key) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - PrintWriter pw = new PrintWriter(out, true); - IndentingPrintWriter writer = new IndentingPrintWriter(pw, ""); - try { - mService.dump(writer); - writer.flush(); - Log.i(TAG, "dump output:" + out.toString()); - for (String line : out.toString().split(System.lineSeparator())) { - String[] vals = line.split(":"); - if (vals[0].equals(key)) { - if (vals.length == 2) { - return vals[1].strip(); - } else { - break; - } - } - } - return ""; - } finally { - writer.close(); - } - } - - List<String> findStringListFromDump(String key) { - String values = findDumpValueForKey(key); - if (values.isEmpty()) { - return Collections.emptyList(); - } - return Arrays.asList(values.split(",")); - } - - private List<String> getFailedPackageNamesPrimary() { - return findStringListFromDump("mFailedPackageNamesPrimary"); - } - - private List<String> getFailedPackageNamesSecondary() { - return findStringListFromDump("mFailedPackageNamesSecondary"); - } - - private int getLastExecutionStatus() { - return Integer.parseInt(findDumpValueForKey("mLastExecutionStatus")); - } - - private static class DexOptInfo { - public final String packageName; - public final boolean isPrimary; - - private DexOptInfo(String packageName, boolean isPrimary) { - this.packageName = packageName; - this.isPrimary = isPrimary; - } - } - - private void addFullRunSequence(@Nullable String expectedSkippedPackage) { - for (String packageName : DEFAULT_PACKAGE_LIST) { - if (packageName.equals(expectedSkippedPackage)) { - // only fails primary dexopt in mocking but add secodary - mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false)); - } else { - mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ true)); - mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false)); - } - } - } - - private static class StartAndWaitThread extends Thread { - private final Runnable mActualRunnable; - private final CountDownLatch mLatch = new CountDownLatch(1); - - private StartAndWaitThread(String name, Runnable runnable) { - super(name); - mActualRunnable = runnable; - } - - private void runActualRunnable() { - mLatch.countDown(); - } - - @Override - public void run() { - // Thread is started but does not run actual code. This is for controlling the execution - // order while still meeting Thread.isAlive() check. - try { - mLatch.await(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - mActualRunnable.run(); - } - } - - private Thread createAndStartExecutionThread(String name, Runnable runnable) { - final boolean isDexOptThread = !name.equals("DexOptCancel"); - StartAndWaitThread thread = new StartAndWaitThread(name, runnable); - if (isDexOptThread) { - mDexOptThread = thread; - } else { - mCancelThread = thread; - } - thread.start(); - return thread; - } -} diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 37967fa86b0f..65986ea063fe 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -62,6 +62,7 @@ android_test { "cts-wm-util", "platform-compat-test-rules", "mockito-target-minus-junit4", + "mockito-kotlin2", "platform-test-annotations", "ShortcutManagerTestUtils", "truth", diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index b2ecea1b0302..9d32ed847645 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -21,7 +21,6 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; -import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG; import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; @@ -50,9 +49,11 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.IAccessibilityServiceClient; import android.app.PendingIntent; import android.app.RemoteAction; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -67,6 +68,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.LocaleList; import android.os.UserHandle; +import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -74,6 +76,7 @@ import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; +import android.util.ArrayMap; import android.util.ArraySet; import android.view.Display; import android.view.DisplayAdjustments; @@ -123,6 +126,7 @@ import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -880,7 +884,6 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_requiredByDefault() { mockManageAccessibilityGranted(mTestableContext); final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME); @@ -889,7 +892,6 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() { mockManageAccessibilityGranted(mTestableContext); final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME); @@ -904,7 +906,6 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() { mockManageAccessibilityGranted(mTestableContext); final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( @@ -925,9 +926,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled({ - FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG, - FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES}) + @RequiresFlagsEnabled(FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES) public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() { mockManageAccessibilityGranted(mTestableContext); final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( @@ -1464,6 +1463,52 @@ public class AccessibilityManagerServiceTest { AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString()); } + @Test + @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + public void restoreAccessibilityQsTargets_a11yQsTargetsRestored() { + String daltonizerTile = + AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(); + String colorInversionTile = + AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString(); + final AccessibilityUserState userState = new AccessibilityUserState( + UserHandle.USER_SYSTEM, mTestableContext, mA11yms); + userState.updateA11yQsTargetLocked(Set.of(daltonizerTile)); + mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState); + + Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED) + .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) + .putExtra(Intent.EXTRA_SETTING_NAME, Settings.Secure.ACCESSIBILITY_QS_TARGETS) + .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, colorInversionTile); + sendBroadcastToAccessibilityManagerService(intent); + mTestableLooper.processAllMessages(); + + assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM).getA11yQsTargets()) + .containsExactlyElementsIn(Set.of(daltonizerTile, colorInversionTile)); + } + + @Test + @RequiresFlagsDisabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT) + public void restoreAccessibilityQsTargets_a11yQsTargetsNotRestored() { + String daltonizerTile = + AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(); + String colorInversionTile = + AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString(); + final AccessibilityUserState userState = new AccessibilityUserState( + UserHandle.USER_SYSTEM, mTestableContext, mA11yms); + userState.updateA11yQsTargetLocked(Set.of(daltonizerTile)); + mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState); + + Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED) + .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) + .putExtra(Intent.EXTRA_SETTING_NAME, Settings.Secure.ACCESSIBILITY_QS_TARGETS) + .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, colorInversionTile); + sendBroadcastToAccessibilityManagerService(intent); + mTestableLooper.processAllMessages(); + + assertThat(userState.getA11yQsTargets()) + .containsExactlyElementsIn(Set.of(daltonizerTile)); + } + private static AccessibilityServiceInfo mockAccessibilityServiceInfo( ComponentName componentName) { return mockAccessibilityServiceInfo( @@ -1542,6 +1587,14 @@ public class AccessibilityManagerServiceTest { mA11yms.getCurrentUserState().updateTileServiceMapForAccessibilityServiceLocked(); } + private void sendBroadcastToAccessibilityManagerService(Intent intent) { + if (!mTestableContext.getBroadcastReceivers().containsKey(intent.getAction())) { + return; + } + mTestableContext.getBroadcastReceivers().get(intent.getAction()).forEach( + broadcastReceiver -> broadcastReceiver.onReceive(mTestableContext, intent)); + } + public static class FakeInputFilter extends AccessibilityInputFilter { FakeInputFilter(Context context, AccessibilityManagerService service) { @@ -1552,6 +1605,7 @@ public class AccessibilityManagerServiceTest { private static class A11yTestableContext extends TestableContext { private final Context mMockContext; + private final Map<String, List<BroadcastReceiver>> mBroadcastReceivers = new ArrayMap<>(); A11yTestableContext(Context base) { super(base); @@ -1563,8 +1617,29 @@ public class AccessibilityManagerServiceTest { mMockContext.startActivityAsUser(intent, options, user); } + @Override + public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user, + IntentFilter filter, String broadcastPermission, Handler scheduler) { + Iterator<String> actions = filter.actionsIterator(); + if (actions != null) { + while (actions.hasNext()) { + String action = actions.next(); + List<BroadcastReceiver> actionReceivers = + mBroadcastReceivers.getOrDefault(action, new ArrayList<>()); + actionReceivers.add(receiver); + mBroadcastReceivers.put(action, actionReceivers); + } + } + return super.registerReceiverAsUser( + receiver, user, filter, broadcastPermission, scheduler); + } + Context getMockContext() { return mMockContext; } + + Map<String, List<BroadcastReceiver>> getBroadcastReceivers() { + return mBroadcastReceivers; + } } } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java index 6e8d6dc3c120..f44879fa54d9 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java @@ -470,6 +470,27 @@ public class AccessibilityWindowManagerWithAccessibilityWindowTest { } @Test + public void onWindowsChanged_shouldNotReportfullyOccludedWindow() { + final AccessibilityWindow frontWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(0); + setRegionForMockAccessibilityWindow(frontWindow, new Region(100, 100, 300, 300)); + final int frontWindowId = mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, frontWindow.getWindowInfo().token); + + // index 1 is focused. Let's use the next one for this test. + final AccessibilityWindow occludedWindow = mWindows.get(Display.DEFAULT_DISPLAY).get(2); + setRegionForMockAccessibilityWindow(occludedWindow, new Region(150, 150, 250, 250)); + final int occludedWindowId = mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, occludedWindow.getWindowInfo().token); + + onAccessibilityWindowsChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + final List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + assertThat(a11yWindows, hasItem(windowId(frontWindowId))); + assertThat(a11yWindows, not(hasItem(windowId(occludedWindowId)))); + } + + @Test public void onWindowsChangedAndForceSend_shouldUpdateWindows() { assertNotEquals("new title", toString(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY) diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 49583ef5194b..a852677c2ed1 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -1318,6 +1318,28 @@ public class BiometricServiceTest { } @Test + public void testDismissedReasonMoreOptions_whilePaused_invokeHalCancel() throws Exception { + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */, null /* authenticators */); + + mBiometricService.mAuthSession.mSensorReceiver.onError( + SENSOR_ID_FACE, + getCookieForCurrentSession(mBiometricService.mAuthSession), + BiometricConstants.BIOMETRIC_ERROR_TIMEOUT, + 0 /* vendorCode */); + mBiometricService.mAuthSession.mSysuiReceiver.onDialogDismissed( + BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS, + null /* credentialAttestation */); + waitForIdle(); + + verify(mReceiver1).onDialogDismissed( + eq(BiometricPrompt.DISMISSED_REASON_CONTENT_VIEW_MORE_OPTIONS)); + verify(mBiometricService.mSensors.get(0).impl) + .cancelAuthenticationFromService(any(), any(), anyLong()); + } + + @Test public void testAcquire_whenAuthenticating_sentToSystemUI() throws Exception { when(mContext.getResources().getString(anyInt())).thenReturn("test string"); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java index a4628ee3b52b..4d1d17f184d1 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java @@ -141,6 +141,7 @@ public class VirtualDeviceTest { @Test public void virtualDevice_hasCustomAudioInputSupport() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS); + mSetFlagsRule.enableFlags(android.media.audiopolicy.Flags.FLAG_AUDIO_MIX_TEST_API); VirtualDevice virtualDevice = new VirtualDevice( @@ -150,6 +151,10 @@ public class VirtualDeviceTest { assertThat(virtualDevice.hasCustomAudioInputSupport()).isFalse(); when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO)).thenReturn(DEVICE_POLICY_CUSTOM); + when(mVirtualDevice.hasCustomAudioInputSupport()).thenReturn(false); + assertThat(virtualDevice.hasCustomAudioInputSupport()).isFalse(); + + when(mVirtualDevice.hasCustomAudioInputSupport()).thenReturn(true); assertThat(virtualDevice.hasCustomAudioInputSupport()).isTrue(); } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java index 4f6fc3dc1f93..0a696ef44897 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java @@ -47,7 +47,7 @@ import android.view.inputmethod.InputMethodInfo; import androidx.annotation.NonNull; import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.R; @@ -67,9 +67,7 @@ import java.util.Set; /** * Run this test with: - * * {@code atest FrameworksServicesTests:com.android.server.devicepolicy.OwnersTest} - * */ @RunWith(AndroidJUnit4.class) public class OverlayPackagesProviderTest { @@ -87,8 +85,8 @@ public class OverlayPackagesProviderTest { private FakePackageManager mPackageManager; private String[] mSystemAppsWithLauncher; - private Set<String> mRegularMainlineModules = new HashSet<>(); - private Map<String, String> mMainlineModuleToDeclaredMetadataMap = new HashMap<>(); + private final Set<String> mRegularMainlineModules = new HashSet<>(); + private final Map<String, String> mMainlineModuleToDeclaredMetadataMap = new HashMap<>(); private OverlayPackagesProvider mHelper; @Before @@ -115,7 +113,8 @@ public class OverlayPackagesProviderTest { setVendorDisallowedAppsManagedUser(); mRealResources = InstrumentationRegistry.getTargetContext().getResources(); - mHelper = new OverlayPackagesProvider(mTestContext, mInjector); + mHelper = new OverlayPackagesProvider(mTestContext, mInjector, + new RecursiveStringArrayResourceResolver(mResources)); } @Test @@ -213,7 +212,7 @@ public class OverlayPackagesProviderTest { } /** - * @see {@link #testAllowedAndDisallowedAtTheSameTimeManagedDevice} + * @see #testAllowedAndDisallowedAtTheSameTimeManagedDevice */ @Test public void testAllowedAndDisallowedAtTheSameTimeManagedUser() { @@ -224,7 +223,7 @@ public class OverlayPackagesProviderTest { } /** - * @see {@link #testAllowedAndDisallowedAtTheSameTimeManagedDevice} + * @see #testAllowedAndDisallowedAtTheSameTimeManagedDevice */ @Test public void testAllowedAndDisallowedAtTheSameTimeManagedProfile() { @@ -447,7 +446,7 @@ public class OverlayPackagesProviderTest { } private void setSystemInputMethods(String... packageNames) { - List<InputMethodInfo> inputMethods = new ArrayList<InputMethodInfo>(); + List<InputMethodInfo> inputMethods = new ArrayList<>(); for (String packageName : packageNames) { ApplicationInfo aInfo = new ApplicationInfo(); aInfo.flags = ApplicationInfo.FLAG_SYSTEM; @@ -467,6 +466,7 @@ public class OverlayPackagesProviderTest { mSystemAppsWithLauncher = apps; } + @SafeVarargs private <T> Set<T> setFromArray(T... array) { if (array == null) { return null; @@ -475,6 +475,7 @@ public class OverlayPackagesProviderTest { } class FakePackageManager extends MockPackageManager { + @NonNull @Override public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId) { assertWithMessage("Expected an intent with action ACTION_MAIN") diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt b/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt new file mode 100644 index 000000000000..647f6c78f29f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.devicepolicy + +import android.annotation.ArrayRes +import android.content.res.Resources +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertWithMessage +import com.google.errorprone.annotations.CanIgnoreReturnValue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + + +/** + * Run this test with: + * `atest FrameworksServicesTests:com.android.server.devicepolicy.RecursiveStringArrayResourceResolverTest` + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class RecursiveStringArrayResourceResolverTest { + private companion object { + const val PACKAGE = "com.android.test" + const val ROOT_RESOURCE = "my_root_resource" + const val SUB_RESOURCE = "my_sub_resource" + const val EXTERNAL_PACKAGE = "com.external.test" + const val EXTERNAL_RESOURCE = "my_external_resource" + } + + private val mResources = mock<Resources>() + private val mTarget = RecursiveStringArrayResourceResolver(mResources) + + /** + * Mocks [Resources.getIdentifier] and [Resources.getStringArray] to return [values] and reference under a generated ID. + * @receiver mocked [Resources] container to configure + * @param pkg package name to "contain" mocked resource + * @param name mocked resource name + * @param values string-array resource values to return when mock is queried + * @return generated resource ID + */ + @ArrayRes + @CanIgnoreReturnValue + private fun Resources.mockStringArrayResource(pkg: String, name: String, vararg values: String): Int { + val anId = (pkg + name).hashCode() + println("Mocking Resources::getIdentifier(name=\"$name\", defType=\"array\", defPackage=\"$pkg\") -> $anId") + whenever(getIdentifier(eq(name), eq("array"), eq(pkg))).thenReturn(anId) + println("Mocking Resources::getStringArray(id=$anId) -> ${values.asList()}") + whenever(getStringArray(eq(anId))).thenReturn(values) + return anId + } + + @Test + fun testCanResolveTheArrayWithoutImports() { + val values = arrayOf("app.a", "app.b") + val mockId = mResources.mockStringArrayResource(pkg = PACKAGE, name = ROOT_RESOURCE, values = values) + + val actual = mTarget.resolve(/* pkg= */ PACKAGE, /* rootId = */ mockId) + + assertWithMessage("Values are resolved correctly") + .that(actual).containsExactlyElementsIn(values) + } + + @Test + fun testCanResolveTheArrayWithImports() { + val externalValues = arrayOf("ext.a", "ext.b", "#import:$PACKAGE/$SUB_RESOURCE") + mResources.mockStringArrayResource(pkg = EXTERNAL_PACKAGE, name = EXTERNAL_RESOURCE, values = externalValues) + val subValues = arrayOf("sub.a", "sub.b") + mResources.mockStringArrayResource(pkg = PACKAGE, name = SUB_RESOURCE, values = subValues) + val values = arrayOf("app.a", "#import:./$SUB_RESOURCE", "app.b", "#import:$EXTERNAL_PACKAGE/$EXTERNAL_RESOURCE", "app.c") + val mockId = mResources.mockStringArrayResource(pkg = PACKAGE, name = ROOT_RESOURCE, values = values) + + val actual = mTarget.resolve(/* pkg= */ PACKAGE, /* rootId= */ mockId) + + assertWithMessage("Values are resolved correctly") + .that(actual).containsExactlyElementsIn((externalValues + subValues + values) + .filterNot { it.startsWith("#import:") } + .toSet()) + } +} diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java index 124970758fa5..3cab75b5d320 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java @@ -2315,10 +2315,11 @@ public class NetworkPolicyManagerServiceTest { } waitForUidEventHandlerIdle(); try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { - // Doesn't cross any other threshold. + // Doesn't cross any threshold, but changes below TOP_THRESHOLD_STATE should always + // be processed callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE - 1, testProcStateSeq++, PROCESS_CAPABILITY_NONE); - assertFalse(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); + assertTrue(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); } waitForUidEventHandlerIdle(); } @@ -2349,21 +2350,21 @@ public class NetworkPolicyManagerServiceTest { int testProcStateSeq = 0; try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { // First callback for uid. - callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + callOnUidStatechanged(UID_A, FOREGROUND_THRESHOLD_STATE, testProcStateSeq++, PROCESS_CAPABILITY_NONE); assertTrue(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); } waitForUidEventHandlerIdle(); try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { // The same process-state with one network capability added. - callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + callOnUidStatechanged(UID_A, FOREGROUND_THRESHOLD_STATE, testProcStateSeq++, PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK); assertTrue(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); } waitForUidEventHandlerIdle(); try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { // The same process-state with another network capability added. - callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + callOnUidStatechanged(UID_A, FOREGROUND_THRESHOLD_STATE, testProcStateSeq++, PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK | PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK); assertTrue(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); @@ -2371,11 +2372,21 @@ public class NetworkPolicyManagerServiceTest { waitForUidEventHandlerIdle(); try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { // The same process-state with all capabilities, but no change in network capabilities. - callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + callOnUidStatechanged(UID_A, FOREGROUND_THRESHOLD_STATE, testProcStateSeq++, PROCESS_CAPABILITY_ALL); assertFalse(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); } waitForUidEventHandlerIdle(); + + callAndWaitOnUidStateChanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + PROCESS_CAPABILITY_ALL); + try (SyncBarrier b = new SyncBarrier(mService.mUidEventHandler)) { + // No change in capabilities, but TOP_THRESHOLD_STATE change should always be processed. + callOnUidStatechanged(UID_A, TOP_THRESHOLD_STATE, testProcStateSeq++, + PROCESS_CAPABILITY_ALL); + assertTrue(mService.mUidEventHandler.hasMessages(UID_MSG_STATE_CHANGED)); + } + waitForUidEventHandlerIdle(); } @Test diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java index 66599e9e9125..510e7c42f12d 100644 --- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java @@ -17,6 +17,8 @@ package com.android.server.power.hint; +import static com.android.server.power.hint.HintManagerService.CLEAN_UP_UID_DELAY_MILLIS; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertArrayEquals; @@ -45,6 +47,9 @@ import android.os.IBinder; import android.os.IHintSession; import android.os.PerformanceHintManager; import android.os.Process; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.Log; import com.android.server.FgThread; @@ -54,11 +59,13 @@ import com.android.server.power.hint.HintManagerService.Injector; import com.android.server.power.hint.HintManagerService.NativeWrapper; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -71,7 +78,7 @@ import java.util.concurrent.locks.LockSupport; * Tests for {@link com.android.server.power.hint.HintManagerService}. * * Build/Install/Run: - * atest FrameworksServicesTests:HintManagerServiceTest + * atest FrameworksServicesTests:HintManagerServiceTest */ public class HintManagerServiceTest { private static final String TAG = "HintManagerServiceTest"; @@ -110,9 +117,15 @@ public class HintManagerServiceTest { makeWorkDuration(2L, 13L, 2L, 8L, 0L), }; - @Mock private Context mContext; - @Mock private HintManagerService.NativeWrapper mNativeWrapperMock; - @Mock private ActivityManagerInternal mAmInternalMock; + @Mock + private Context mContext; + @Mock + private HintManagerService.NativeWrapper mNativeWrapperMock; + @Mock + private ActivityManagerInternal mAmInternalMock; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); private HintManagerService mService; @@ -122,12 +135,11 @@ public class HintManagerServiceTest { when(mNativeWrapperMock.halGetHintSessionPreferredRate()) .thenReturn(DEFAULT_HINT_PREFERRED_RATE); when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A), - eq(DEFAULT_TARGET_DURATION))).thenReturn(1L); + eq(DEFAULT_TARGET_DURATION))).thenReturn(1L); when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_B), - eq(DEFAULT_TARGET_DURATION))).thenReturn(2L); + eq(DEFAULT_TARGET_DURATION))).thenReturn(2L); when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_C), - eq(0L))).thenReturn(1L); - when(mAmInternalMock.getIsolatedProcesses(anyInt())).thenReturn(null); + eq(0L))).thenReturn(1L); LocalServices.removeServiceForTest(ActivityManagerInternal.class); LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock); } @@ -434,6 +446,163 @@ public class HintManagerServiceTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_POWERHINT_THREAD_CLEANUP) + public void testCleanupDeadThreads() throws Exception { + HintManagerService service = createService(); + IBinder token = new Binder(); + CountDownLatch stopLatch1 = new CountDownLatch(1); + int threadCount = 3; + int[] tids1 = createThreads(threadCount, stopLatch1); + long sessionPtr1 = 111; + when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids1), + eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr1); + AppHintSession session1 = (AppHintSession) service.getBinderServiceInstance() + .createHintSession(token, tids1, DEFAULT_TARGET_DURATION); + assertNotNull(session1); + + // for test only to avoid conflicting with any real thread that exists on device + int isoProc1 = -100; + int isoProc2 = 9999; + when(mAmInternalMock.getIsolatedProcesses(eq(UID))).thenReturn(List.of(0)); + + CountDownLatch stopLatch2 = new CountDownLatch(1); + int[] tids2 = createThreads(threadCount, stopLatch2); + int[] tids2WithIsolated = Arrays.copyOf(tids2, tids2.length + 2); + int[] expectedTids2 = Arrays.copyOf(tids2, tids2.length + 1); + expectedTids2[tids2.length] = isoProc1; + tids2WithIsolated[threadCount] = isoProc1; + tids2WithIsolated[threadCount + 1] = isoProc2; + long sessionPtr2 = 222; + when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids2WithIsolated), + eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr2); + AppHintSession session2 = (AppHintSession) service.getBinderServiceInstance() + .createHintSession(token, tids2WithIsolated, DEFAULT_TARGET_DURATION); + assertNotNull(session2); + + // trigger clean up through UID state change by making the process background + service.mUidObserver.onUidStateChanged(UID, + ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos( + CLEAN_UP_UID_DELAY_MILLIS)); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any()); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any()); + // the new TIDs pending list should be updated + assertArrayEquals(session2.getTidsInternal(), expectedTids2); + reset(mNativeWrapperMock); + + // this should resume and update the threads so those never-existed invalid isolated + // processes should be cleaned up + service.mUidObserver.onUidStateChanged(UID, + ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0); + // wait for the async uid state change to trigger resume and setThreads + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500)); + verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2), eq(expectedTids2)); + reset(mNativeWrapperMock); + + // let all session 1 threads to exit and the cleanup should force pause the session + stopLatch1.countDown(); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100)); + service.mUidObserver.onUidStateChanged(UID, + ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos( + CLEAN_UP_UID_DELAY_MILLIS)); + verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1)); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any()); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any()); + // all hints will have no effect as the session is force paused while proc in foreground + verifyAllHintsEnabled(session1, false); + verifyAllHintsEnabled(session2, true); + reset(mNativeWrapperMock); + + // in foreground, set new tids for session 1 then it should be resumed and all hints allowed + stopLatch1 = new CountDownLatch(1); + tids1 = createThreads(threadCount, stopLatch1); + session1.setThreads(tids1); + verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1), eq(tids1)); + verify(mNativeWrapperMock, times(1)).halResumeHintSession(eq(sessionPtr1)); + verifyAllHintsEnabled(session1, true); + reset(mNativeWrapperMock); + + // let all session 1 and 2 non isolated threads to exit + stopLatch1.countDown(); + stopLatch2.countDown(); + expectedTids2 = new int[]{isoProc1}; + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100)); + service.mUidObserver.onUidStateChanged(UID, + ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos( + CLEAN_UP_UID_DELAY_MILLIS)); + verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1)); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any()); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any()); + // in background, set threads for session 1 then it should not be force paused next time + session1.setThreads(SESSION_TIDS_A); + // the new TIDs pending list should be updated + assertArrayEquals(session1.getTidsInternal(), SESSION_TIDS_A); + assertArrayEquals(session2.getTidsInternal(), expectedTids2); + verifyAllHintsEnabled(session1, false); + verifyAllHintsEnabled(session2, false); + reset(mNativeWrapperMock); + + service.mUidObserver.onUidStateChanged(UID, + ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos( + CLEAN_UP_UID_DELAY_MILLIS)); + verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1), + eq(SESSION_TIDS_A)); + verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2), + eq(expectedTids2)); + verifyAllHintsEnabled(session1, true); + verifyAllHintsEnabled(session2, true); + } + + private void verifyAllHintsEnabled(AppHintSession session, boolean verifyEnabled) { + session.reportActualWorkDuration2(new WorkDuration[]{makeWorkDuration(1, 3, 2, 1, 1000)}); + session.reportActualWorkDuration(new long[]{1}, new long[]{2}); + session.updateTargetWorkDuration(3); + session.setMode(0, true); + session.sendHint(1); + if (verifyEnabled) { + verify(mNativeWrapperMock, times(1)).halReportActualWorkDuration( + eq(session.mHalSessionPtr), any()); + verify(mNativeWrapperMock, times(1)).halSetMode(eq(session.mHalSessionPtr), anyInt(), + anyBoolean()); + verify(mNativeWrapperMock, times(1)).halUpdateTargetWorkDuration( + eq(session.mHalSessionPtr), anyLong()); + verify(mNativeWrapperMock, times(1)).halSendHint(eq(session.mHalSessionPtr), anyInt()); + } else { + verify(mNativeWrapperMock, never()).halReportActualWorkDuration( + eq(session.mHalSessionPtr), any()); + verify(mNativeWrapperMock, never()).halSetMode(eq(session.mHalSessionPtr), anyInt(), + anyBoolean()); + verify(mNativeWrapperMock, never()).halUpdateTargetWorkDuration( + eq(session.mHalSessionPtr), anyLong()); + verify(mNativeWrapperMock, never()).halSendHint(eq(session.mHalSessionPtr), anyInt()); + } + } + + private int[] createThreads(int threadCount, CountDownLatch stopLatch) + throws InterruptedException { + int[] tids = new int[threadCount]; + AtomicInteger k = new AtomicInteger(0); + CountDownLatch latch = new CountDownLatch(threadCount); + for (int j = 0; j < threadCount; j++) { + Thread thread = new Thread(() -> { + try { + tids[k.getAndIncrement()] = android.os.Process.myTid(); + latch.countDown(); + stopLatch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + thread.start(); + } + latch.await(); + return tids; + } + + @Test public void testSetMode() throws Exception { HintManagerService service = createService(); IBinder token = new Binder(); @@ -457,7 +626,8 @@ public class HintManagerServiceTest { // Set session to background, then the duration would not be updated. service.mUidObserver.onUidStateChanged( a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); - FgThread.getHandler().runWithScissors(() -> { }, 500); + FgThread.getHandler().runWithScissors(() -> { + }, 500); assertFalse(service.mUidObserver.isUidForeground(a.mUid)); a.setMode(0, true); verify(mNativeWrapperMock, never()).halSetMode(anyLong(), anyInt(), anyBoolean()); @@ -519,7 +689,10 @@ public class HintManagerServiceTest { LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500)); service.mUidObserver.onUidStateChanged(UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); - LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500)); + // let the cleanup work proceed + LockSupport.parkNanos( + TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos( + CLEAN_UP_UID_DELAY_MILLIS)); } Log.d(TAG, "notifier thread min " + min + " max " + max + " avg " + sum / count); service.mUidObserver.onUidGone(UID, true); diff --git a/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING new file mode 100644 index 000000000000..2d5df077b128 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING @@ -0,0 +1,15 @@ +{ + "postsubmit": [ + { + "name": "FrameworksServicesTests", + "options": [ + { + "include-filter": "com.android.server.power.hint" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ] +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 99ab40569b70..06a4ac932e8e 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -5862,6 +5862,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(captor.getValue().getNotification().flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo( FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); + assertThat(captor.getValue().shouldPostSilently()).isTrue(); } @Test @@ -8603,6 +8604,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(captor.getValue().getNotification().flags & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo( FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY); + assertThat(captor.getValue().shouldPostSilently()).isTrue(); } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index bfc47fdef5cb..cee6cdb06bf5 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -3962,6 +3962,20 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void testReadXml_existingPackage_bubblePrefsRestored() throws Exception { + mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_ALL); + assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O)); + + mXmlHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE); + assertEquals(BUBBLE_PREFERENCE_NONE, mXmlHelper.getBubblePreference(PKG_O, UID_O)); + + ByteArrayOutputStream stream = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL); + loadStreamXml(stream, true, UserHandle.USER_ALL); + + assertEquals(BUBBLE_PREFERENCE_ALL, mXmlHelper.getBubblePreference(PKG_O, UID_O)); + } + + @Test public void testUpdateNotificationChannel_fixedPermission() { List<UserInfo> users = ImmutableList.of(new UserInfo(UserHandle.USER_SYSTEM, "user0", 0)); when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(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 8cbcc226ce73..5861d88924e0 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -500,7 +500,8 @@ public class VibratorManagerServiceTest { InOrder batteryVerifier = inOrder(mBatteryStatsMock); batteryVerifier.verify(mBatteryStatsMock) .noteVibratorOn(UID, oneShotDuration + mVibrationConfig.getRampDownDurationMs()); - batteryVerifier.verify(mBatteryStatsMock).noteVibratorOff(UID); + batteryVerifier + .verify(mBatteryStatsMock, timeout(TEST_TIMEOUT_MILLIS)).noteVibratorOff(UID); } @Test diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index 29467f259ac3..a80e2f8ae28c 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -16,10 +16,14 @@ package com.android.server.policy; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManagerGlobal.ADD_OKAY; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; @@ -33,18 +37,27 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import android.app.ActivityManager; import android.app.AppOpsManager; +import android.content.Context; +import android.os.PowerManager; import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.filters.SmallTest; +import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.DisplayPolicy; +import com.android.server.wm.DisplayRotation; +import com.android.server.wm.WindowManagerInternal; import org.junit.After; import org.junit.Before; @@ -64,16 +77,27 @@ public class PhoneWindowManagerTests { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); PhoneWindowManager mPhoneWindowManager; + private ActivityTaskManagerInternal mAtmInternal; + private Context mContext; @Before public void setUp() { mPhoneWindowManager = spy(new PhoneWindowManager()); spyOn(ActivityManager.getService()); + mContext = getInstrumentation().getTargetContext(); + spyOn(mContext); + mAtmInternal = mock(ActivityTaskManagerInternal.class); + LocalServices.addService(ActivityTaskManagerInternal.class, mAtmInternal); + mPhoneWindowManager.mActivityTaskManagerInternal = mAtmInternal; + LocalServices.addService(WindowManagerInternal.class, mock(WindowManagerInternal.class)); } @After public void tearDown() { reset(ActivityManager.getService()); + reset(mContext); + LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); + LocalServices.removeServiceForTest(WindowManagerInternal.class); } @Test @@ -99,6 +123,60 @@ public class PhoneWindowManagerTests { } @Test + public void testScreenTurnedOff() { + mSetFlagsRule.enableFlags(com.android.window.flags.Flags + .FLAG_SKIP_SLEEPING_WHEN_SWITCHING_DISPLAY); + doNothing().when(mPhoneWindowManager).updateSettings(any()); + doNothing().when(mPhoneWindowManager).initializeHdmiState(); + final boolean[] isScreenTurnedOff = { false }; + final DisplayPolicy displayPolicy = mock(DisplayPolicy.class); + doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff(); + doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnEarly(); + doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnFully(); + + mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy; + mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class); + final ActivityTaskManagerInternal.SleepTokenAcquirer tokenAcquirer = + mock(ActivityTaskManagerInternal.SleepTokenAcquirer.class); + doReturn(tokenAcquirer).when(mAtmInternal).createSleepTokenAcquirer(anyString()); + final PowerManager pm = mock(PowerManager.class); + doReturn(true).when(pm).isInteractive(); + doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE)); + + mContext.getMainThreadHandler().runWithScissors(() -> mPhoneWindowManager.init( + new PhoneWindowManager.Injector(mContext, + mock(WindowManagerPolicy.WindowManagerFuncs.class))), 0); + assertThat(isScreenTurnedOff[0]).isFalse(); + assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse(); + + // Skip sleep-token for non-sleep-screen-off. + clearInvocations(tokenAcquirer); + mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); + verify(tokenAcquirer, never()).acquire(anyInt(), anyBoolean()); + assertThat(isScreenTurnedOff[0]).isTrue(); + + // Apply sleep-token for sleep-screen-off. + mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); + assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isTrue(); + mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); + verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY), eq(true)); + + mPhoneWindowManager.finishedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); + assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse(); + + // Simulate unexpected reversed order: screenTurnedOff -> startedGoingToSleep. The sleep + // token can still be acquired. + isScreenTurnedOff[0] = false; + clearInvocations(tokenAcquirer); + mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); + verify(tokenAcquirer, never()).acquire(anyInt(), anyBoolean()); + assertThat(displayPolicy.isScreenOnEarly()).isFalse(); + assertThat(displayPolicy.isScreenOnFully()).isFalse(); + mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); + verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY), eq(false)); + } + + @Test public void testCheckAddPermission_withoutAccessibilityOverlay_noAccessibilityAppOpLogged() { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); @@ -130,11 +208,8 @@ public class PhoneWindowManagerTests { private void mockStartDockOrHome() throws Exception { doNothing().when(ActivityManager.getService()).stopAppSwitches(); - ActivityTaskManagerInternal mMockActivityTaskManagerInternal = - mock(ActivityTaskManagerInternal.class); - when(mMockActivityTaskManagerInternal.startHomeOnDisplay( + when(mAtmInternal.startHomeOnDisplay( anyInt(), anyString(), anyInt(), anyBoolean(), anyBoolean())).thenReturn(false); - mPhoneWindowManager.mActivityTaskManagerInternal = mMockActivityTaskManagerInternal; mPhoneWindowManager.mUserManagerInternal = mock(UserManagerInternal.class); } } diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java index 0a29dfbd7db7..60716cbbb693 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java @@ -95,8 +95,6 @@ public class ShortcutLoggingTests extends ShortcutKeyTestBase { new int[]{KeyEvent.KEYCODE_NOTIFICATION}, KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_NOTIFICATION, 0}, - {"Meta + T -> Toggle Taskbar", new int[]{META_KEY, KeyEvent.KEYCODE_T}, - KeyboardLogEvent.TOGGLE_TASKBAR, KeyEvent.KEYCODE_T, META_ON}, {"Meta + Ctrl + S -> Take Screenshot", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S}, KeyboardLogEvent.TAKE_SCREENSHOT, KeyEvent.KEYCODE_S, META_ON | CTRL_ON}, diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index daa5a5a4fccc..beafeec20bb5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -3346,7 +3346,7 @@ public class ActivityRecordTests extends WindowTestsBase { } else { verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(), insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(), - anyBoolean()); + anyBoolean(), any()); } assertFalse(app2.getInsetsState().isSourceOrDefaultVisible(ID_IME, ime())); } @@ -3416,6 +3416,7 @@ public class ActivityRecordTests extends WindowTestsBase { // Remove window during transition, so it is requested to hide, but won't be committed until // the transition is finished. app.mActivityRecord.onRemovedFromDisplay(); + app.mActivityRecord.prepareSurfaces(); assertTrue(mDisplayContent.mClosingApps.contains(app.mActivityRecord)); assertFalse(app.mActivityRecord.isVisibleRequested()); @@ -3433,6 +3434,7 @@ public class ActivityRecordTests extends WindowTestsBase { public void testInClosingAnimation_visibilityCommitted_hideSurface() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); makeWindowVisibleAndDrawn(app); + app.mActivityRecord.prepareSurfaces(); // Put the activity in close transition. mDisplayContent.mOpeningApps.clear(); diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index c29547f123aa..b9e87dc6efce 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -633,18 +633,23 @@ public class BackNavigationControllerTests extends WindowTestsBase { @Test public void testAdjacentFocusInActivityEmbedding() { mSetFlagsRule.enableFlags(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG); - Task task = createTask(mDefaultDisplay); - TaskFragment primary = createTaskFragmentWithActivity(task); - TaskFragment secondary = createTaskFragmentWithActivity(task); - primary.setAdjacentTaskFragment(secondary); - secondary.setAdjacentTaskFragment(primary); - - WindowState windowState = mock(WindowState.class); + final Task task = createTask(mDefaultDisplay); + final TaskFragment primaryTf = createTaskFragmentWithActivity(task); + final TaskFragment secondaryTf = createTaskFragmentWithActivity(task); + final ActivityRecord primaryActivity = primaryTf.getTopMostActivity(); + final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity(); + primaryTf.setAdjacentTaskFragment(secondaryTf); + secondaryTf.setAdjacentTaskFragment(primaryTf); + + final WindowState windowState = mock(WindowState.class); + windowState.mActivityRecord = primaryActivity; doReturn(windowState).when(mWm).getFocusedWindowLocked(); - doReturn(primary).when(windowState).getTaskFragment(); + doReturn(primaryTf).when(windowState).getTaskFragment(); + doReturn(1L).when(primaryActivity).getLastWindowCreateTime(); + doReturn(2L).when(secondaryActivity).getLastWindowCreateTime(); startBackNavigation(); - verify(mWm).moveFocusToActivity(any()); + verify(mWm).moveFocusToActivity(eq(secondaryActivity)); } /** diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 4e360d06ce6a..2c88ed2db2d6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1068,16 +1068,6 @@ public class DisplayContentTests extends WindowTestsBase { mDisplayContent.getImeTarget(IME_TARGET_LAYERING)); } - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testInputMethodSet_listenOnDisplayAreaConfigurationChanged() { - spyOn(mAtm); - mDisplayContent.setInputMethodWindowLocked(mImeWindow); - - verify(mAtm).onImeWindowSetOnDisplayArea( - mImeWindow.mSession.mPid, mDisplayContent.getImeContainer()); - } - @Test public void testAllowsTopmostFullscreenOrientation() { final DisplayContent dc = createNewDisplay(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 897a3da07473..52485eec8505 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -25,7 +25,7 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; -import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK; @@ -1835,7 +1835,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final TaskFragment tf = createTaskFragment(task); final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE).build(); + OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE).build(); mTransaction.addTaskFragmentOperation(tf.getFragmentToken(), operation); assertApplyTransactionAllowed(mTransaction); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 5360a1033eb4..6b1bf26bfdff 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -887,20 +887,14 @@ public class TaskFragmentTest extends WindowTestsBase { assertEquals(winLeftTop, mDisplayContent.mCurrentFocus); if (Flags.embeddedActivityBackNavFlag()) { - // Send request to move the focus to top window from the left window. - assertTrue(mWm.moveFocusToTopEmbeddedWindow(winLeftTop)); - // The focus should change. - assertEquals(winRightTop, mDisplayContent.mCurrentFocus); - - // Send request to move the focus to top window from the right window. - assertFalse(mWm.moveFocusToTopEmbeddedWindow(winRightTop)); - // The focus should NOT change. - assertEquals(winRightTop, mDisplayContent.mCurrentFocus); - - // Do not move focus if the dim is boosted. - taskFragmentLeft.mDimmerSurfaceBoosted = true; - assertFalse(mWm.moveFocusToTopEmbeddedWindow(winLeftTop)); - assertEquals(winRightTop, mDisplayContent.mCurrentFocus); + // Move focus if the adjacent activity is more recently active. + doReturn(1L).when(appLeftTop).getLastWindowCreateTime(); + doReturn(2L).when(appRightTop).getLastWindowCreateTime(); + assertTrue(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop)); + + // Do not move the focus if the adjacent activity is less recently active. + doReturn(3L).when(appLeftTop).getLastWindowCreateTime(); + assertFalse(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop)); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 3bd6496a01dd..a88680a002b9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -1945,6 +1945,21 @@ public class TaskTests extends WindowTestsBase { assertEquals(2, finishCount[0]); } + @Test + public void testPauseActivityWhenHasEmptyLeafTaskFragment() { + // Creating a task that has a RESUMED activity and an empty TaskFragment. + final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); + final ActivityRecord activity = task.getTopMostActivity(); + new TaskFragmentBuilder(mAtm).setParentTask(task).build(); + activity.setState(ActivityRecord.State.RESUMED, "test"); + + // Ensure the activity is paused if cannot be resumed. + doReturn(false).when(task).canBeResumed(any()); + mSupervisor.mUserLeaving = true; + task.pauseActivityIfNeeded(null /* resuming */, "test"); + verify(task).startPausing(eq(true) /* userLeaving */, anyBoolean(), any(), any()); + } + private Task getTestTask() { return new TaskBuilder(mSupervisor).setCreateActivity(true).build(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java index 3f8acc651110..37de51eccff2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java @@ -28,6 +28,7 @@ import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.ScrollCaptureResponse; import android.view.inputmethod.ImeTracker; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import com.android.internal.os.IResultReceiver; @@ -46,8 +47,8 @@ public class TestIWindow extends IWindow.Stub { @Override public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfig, InsetsState insetsState, boolean forceLayout, - boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing) - throws RemoteException { + boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing, + @Nullable ActivityWindowInfo activityWindowInfo) throws RemoteException { } @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 12f46df451fe..48b12f729e08 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -90,6 +90,7 @@ import android.util.ArraySet; import android.util.MergedConfiguration; import android.view.ContentRecordingSession; import android.view.IWindow; +import android.view.IWindowSession; import android.view.InputChannel; import android.view.InsetsSourceControl; import android.view.InsetsState; @@ -99,6 +100,7 @@ import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.InputTransferToken; import android.window.ScreenCapture; @@ -1216,6 +1218,35 @@ public class WindowManagerServiceTests extends WindowTestsBase { mWm.reportKeepClearAreasChanged(session, window, new ArrayList<>(), new ArrayList<>()); } + @Test + public void testRelayout_appWindowSendActivityWindowInfo() { + mSetFlagsRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); + + // Skip unnecessary operations of relayout. + spyOn(mWm.mWindowPlacerLocked); + doNothing().when(mWm.mWindowPlacerLocked).performSurfacePlacement(anyBoolean()); + + final Task task = createTask(mDisplayContent); + final WindowState win = createAppWindow(task, ACTIVITY_TYPE_STANDARD, "appWindow"); + mWm.mWindowMap.put(win.mClient.asBinder(), win); + + final int w = 100; + final int h = 200; + final ClientWindowFrames outFrames = new ClientWindowFrames(); + final MergedConfiguration outConfig = new MergedConfiguration(); + final SurfaceControl outSurfaceControl = new SurfaceControl(); + final InsetsState outInsetsState = new InsetsState(); + final InsetsSourceControl.Array outControls = new InsetsSourceControl.Array(); + final Bundle outBundle = new Bundle(); + + mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.GONE, 0, 0, 0, + outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle); + + final ActivityWindowInfo activityWindowInfo = outBundle.getParcelable( + IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO, ActivityWindowInfo.class); + assertEquals(win.mActivityRecord.getActivityWindowInfo(), activityWindowInfo); + } + class TestResultReceiver implements IResultReceiver { public android.os.Bundle resultData; private final IBinder mBinder = mock(IBinder.class); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index c8ad4bd47880..e20f8227612b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -804,7 +804,8 @@ public class WindowStateTests extends WindowTestsBase { anyBoolean() /* reportDraw */, any() /* mergedConfig */, any() /* insetsState */, anyBoolean() /* forceLayout */, anyBoolean() /* alwaysConsumeSystemBars */, anyInt() /* displayId */, - anyInt() /* seqId */, anyBoolean() /* dragResizing */); + anyInt() /* seqId */, anyBoolean() /* dragResizing */, + any() /* activityWindowInfo */); } catch (RemoteException ignored) { } win.reportResized(); diff --git a/services/usage/java/com/android/server/usage/StorageStatsService.java b/services/usage/java/com/android/server/usage/StorageStatsService.java index 883c702ddb79..e9da53a8a899 100644 --- a/services/usage/java/com/android/server/usage/StorageStatsService.java +++ b/services/usage/java/com/android/server/usage/StorageStatsService.java @@ -968,22 +968,20 @@ public class StorageStatsService extends IStorageStatsManager.Stub { stats.libSize += getDirBytes(new File(sourceDirName + "/lib/")); // Get dexopt, current profle and reference profile sizes. - if (SystemProperties.getBoolean("dalvik.vm.features.art_managed_file_stats", false)) { - ArtManagedFileStats artManagedFileStats; - try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) { - artManagedFileStats = - getArtManagerLocal().getArtManagedFileStats(snapshot, packageName); - } - - stats.dexoptSize += - artManagedFileStats - .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_DEXOPT_ARTIFACT); - stats.refProfSize += - artManagedFileStats - .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_REF_PROFILE); - stats.curProfSize += - artManagedFileStats - .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_CUR_PROFILE); - } + ArtManagedFileStats artManagedFileStats; + try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) { + artManagedFileStats = + getArtManagerLocal().getArtManagedFileStats(snapshot, packageName); + } + + stats.dexoptSize += + artManagedFileStats + .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_DEXOPT_ARTIFACT); + stats.refProfSize += + artManagedFileStats + .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_REF_PROFILE); + stats.curProfSize += + artManagedFileStats + .getTotalSizeBytesByType(ArtManagedFileStats.TYPE_CUR_PROFILE); } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java index ad7b9e69282e..96c3ed5767f6 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java @@ -184,7 +184,7 @@ abstract class DetectorSession { private final Executor mAudioCopyExecutor = Executors.newCachedThreadPool(); // TODO: This may need to be a Handler(looper) final ScheduledExecutorService mScheduledExecutorService; - private final AppOpsManager mAppOpsManager; + final AppOpsManager mAppOpsManager; final HotwordAudioStreamCopier mHotwordAudioStreamCopier; final AtomicBoolean mUpdateStateAfterStartFinished = new AtomicBoolean(false); final IHotwordRecognitionStatusCallback mCallback; @@ -201,7 +201,7 @@ abstract class DetectorSession { /** Identity used for attributing app ops when delivering data to the Interactor. */ @Nullable - private final Identity mVoiceInteractorIdentity; + final Identity mVoiceInteractorIdentity; @GuardedBy("mLock") ParcelFileDescriptor mCurrentAudioSink; @GuardedBy("mLock") @@ -926,7 +926,7 @@ abstract class DetectorSession { * @param permission The identifier of the permission we want to check. * @param reason The reason why we're requesting the permission, for auditing purposes. */ - private static void enforcePermissionForDataDelivery(@NonNull Context context, + protected static void enforcePermissionForDataDelivery(@NonNull Context context, @NonNull Identity identity, @NonNull String permission, @NonNull String reason) { final int status = PermissionUtil.checkPermissionForDataDelivery(context, identity, permission, reason); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java index aef8e6fabc9b..0a660658338d 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VisualQueryDetectorSession.java @@ -16,6 +16,10 @@ package com.android.server.voiceinteraction; +import static android.Manifest.permission.CAMERA; +import static android.Manifest.permission.RECORD_AUDIO; +import static android.app.AppOpsManager.OP_CAMERA; +import static android.app.AppOpsManager.OP_RECORD_AUDIO; import static android.service.voice.VisualQueryDetectionServiceFailure.ERROR_CODE_ILLEGAL_ATTENTION_STATE; import static android.service.voice.VisualQueryDetectionServiceFailure.ERROR_CODE_ILLEGAL_STREAMING_STATE; @@ -24,6 +28,7 @@ import android.annotation.Nullable; import android.content.Context; import android.media.AudioFormat; import android.media.permission.Identity; +import android.os.Binder; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; @@ -58,6 +63,14 @@ import java.util.concurrent.ScheduledExecutorService; final class VisualQueryDetectorSession extends DetectorSession { private static final String TAG = "VisualQueryDetectorSession"; + + private static final String VISUAL_QUERY_DETECTION_AUDIO_OP_MESSAGE = + "Providing query detection result from VisualQueryDetectionService to " + + "VoiceInteractionService"; + + private static final String VISUAL_QUERY_DETECTION_CAMERA_OP_MESSAGE = + "Providing query detection result from VisualQueryDetectionService to " + + "VoiceInteractionService"; private IVisualQueryDetectionAttentionListener mAttentionListener; private boolean mEgressingData; private boolean mQueryStreaming; @@ -172,6 +185,22 @@ final class VisualQueryDetectorSession extends DetectorSession { "Cannot stream queries without attention signals.")); return; } + try { + enforcePermissionsForVisualQueryDelivery(RECORD_AUDIO, OP_RECORD_AUDIO, + VISUAL_QUERY_DETECTION_AUDIO_OP_MESSAGE); + } catch (SecurityException e) { + Slog.w(TAG, "Ignoring #onQueryDetected due to a SecurityException", e); + try { + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_STREAMING_STATE, + "Cannot stream queries without audio permission.")); + } catch (RemoteException e1) { + notifyOnDetectorRemoteException(); + throw e1; + } + return; + } mQueryStreaming = true; callback.onQueryDetected(partialQuery); Slog.i(TAG, "Egressed from visual query detection process."); @@ -202,6 +231,48 @@ final class VisualQueryDetectorSession extends DetectorSession { + "enabling the setting.")); return; } + + // Show camera icon if visual only accessibility data egresses + if (partialResult.getAccessibilityDetectionData() != null) { + try { + enforcePermissionsForVisualQueryDelivery(CAMERA, OP_CAMERA, + VISUAL_QUERY_DETECTION_CAMERA_OP_MESSAGE); + } catch (SecurityException e) { + Slog.w(TAG, "Ignoring #onQueryDetected due to a SecurityException", e); + try { + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_STREAMING_STATE, + "Cannot stream visual only accessibility data " + + "without camera permission.")); + } catch (RemoteException e1) { + notifyOnDetectorRemoteException(); + throw e1; + } + return; + } + } + + // Show microphone icon if text query egresses + if (!partialResult.getPartialQuery().isEmpty()) { + try { + enforcePermissionsForVisualQueryDelivery(RECORD_AUDIO, OP_RECORD_AUDIO, + VISUAL_QUERY_DETECTION_AUDIO_OP_MESSAGE); + } catch (SecurityException e) { + Slog.w(TAG, "Ignoring #onQueryDetected due to a SecurityException", e); + try { + callback.onVisualQueryDetectionServiceFailure( + new VisualQueryDetectionServiceFailure( + ERROR_CODE_ILLEGAL_STREAMING_STATE, + "Cannot stream queries without audio permission.")); + } catch (RemoteException e1) { + notifyOnDetectorRemoteException(); + throw e1; + } + return; + } + } + mQueryStreaming = true; callback.onResultDetected(partialResult); Slog.i(TAG, "Egressed from visual query detection process."); @@ -280,6 +351,20 @@ final class VisualQueryDetectorSession extends DetectorSession { mEnableAccessibilityDataEgress = enable; } + void enforcePermissionsForVisualQueryDelivery(String permission, int op, String msg) { + Binder.withCleanCallingIdentity(() -> { + synchronized (mLock) { + enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity, + permission, msg); + mAppOpsManager.noteOpNoThrow( + op, mVoiceInteractorIdentity.uid, + mVoiceInteractorIdentity.packageName, + mVoiceInteractorIdentity.attributionTag, + msg); + } + }); + } + @SuppressWarnings("GuardedBy") public void dumpLocked(String prefix, PrintWriter pw) { super.dumpLocked(prefix, pw); diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index a047b97d8592..69c47d43e2b8 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -15021,16 +15021,15 @@ public class TelephonyManager { */ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @FlaggedApi(android.permission.flags.Flags.FLAG_GET_EMERGENCY_ROLE_HOLDER_API_ENABLED) - @NonNull + @Nullable @SystemApi public String getEmergencyAssistancePackageName() { if (!isEmergencyAssistanceEnabled() || !isVoiceCapable()) { throw new IllegalStateException("isEmergencyAssistanceEnabled() is false or device" + " not voice capable."); } - String emergencyRole = mContext.getSystemService(RoleManager.class) + return mContext.getSystemService(RoleManager.class) .getEmergencyRoleHolder(mContext.getUserId()); - return Objects.requireNonNull(emergencyRole, "Emergency role holder must not be null"); } /** diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl index 9441fb5d02ef..36485c6b6fb5 100644 --- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl +++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl @@ -347,28 +347,6 @@ oneway interface ISatellite { in IIntegerConsumer callback); /** - * Request to get whether satellite communication is allowed for the current location. - * - * @param resultCallback The callback to receive the error code result of the operation. - * This must only be sent when the result is not - * SatelliteResult#SATELLITE_RESULT_SUCCESS. - * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to - * receive whether satellite communication is allowed for the current location. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - */ - void requestIsSatelliteCommunicationAllowedForCurrentLocation( - in IIntegerConsumer resultCallback, in IBooleanConsumer callback); - - /** * Request to get the time after which the satellite will be visible. This is an int * representing the duration in seconds after which the satellite will be visible. * This will return 0 if the satellite is currently visible. diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java index f17ff17497f2..b7dc79ff7283 100644 --- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java +++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java @@ -194,17 +194,6 @@ public class SatelliteImplBase extends SatelliteService { } @Override - public void requestIsSatelliteCommunicationAllowedForCurrentLocation( - IIntegerConsumer resultCallback, IBooleanConsumer callback) - throws RemoteException { - executeMethodAsync( - () -> SatelliteImplBase.this - .requestIsSatelliteCommunicationAllowedForCurrentLocation( - resultCallback, callback), - "requestIsCommunicationAllowedForCurrentLocation"); - } - - @Override public void requestTimeForNextSatelliteVisibility(IIntegerConsumer resultCallback, IIntegerConsumer callback) throws RemoteException { executeMethodAsync( @@ -638,30 +627,6 @@ public class SatelliteImplBase extends SatelliteService { } /** - * Request to get whether satellite communication is allowed for the current location. - * - * @param resultCallback The callback to receive the error code result of the operation. - * This must only be sent when the result is not - * SatelliteResult#SATELLITE_RESULT_SUCCESS. - * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to - * receive whether satellite communication is allowed for the current location. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - */ - public void requestIsSatelliteCommunicationAllowedForCurrentLocation( - @NonNull IIntegerConsumer resultCallback, @NonNull IBooleanConsumer callback) { - // stub implementation - } - - /** * Request to get the time after which the satellite will be visible. This is an int * representing the duration in seconds after which the satellite will be visible. * This will return 0 if the satellite is currently visible. diff --git a/test-base/Android.bp b/test-base/Android.bp index 70a95400bd9e..d65a4e44440e 100644 --- a/test-base/Android.bp +++ b/test-base/Android.bp @@ -14,37 +14,22 @@ // limitations under the License. // +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + // Build the android.test.base library // =================================== // This contains the junit.framework and android.test classes that were in // Android API level 25 excluding those from android.test.runner. // Also contains the com.android.internal.util.Predicate[s] classes. -package { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - // SPDX-license-identifier-CPL-1.0 - default_applicable_licenses: ["frameworks_base_test-base_license"], -} - -license { - name: "frameworks_base_test-base_license", - visibility: [":__subpackages__"], - license_kinds: [ - "SPDX-license-identifier-Apache-2.0", - "SPDX-license-identifier-CPL-1.0", - ], - license_text: [ - "src/junit/cpl-v10.html", - ], -} - java_sdk_library { name: "android.test.base", - srcs: [":android-test-base-sources"], + srcs: [ + ":android-test-base-sources", + ":frameworks-base-test-junit-framework", + ], errorprone: { javacflags: ["-Xep:DepAnn:ERROR"], @@ -84,7 +69,10 @@ java_library_static { ], installable: false, - srcs: [":android-test-base-sources"], + srcs: [ + ":android-test-base-sources", + ":frameworks-base-test-junit-framework", + ], errorprone: { javacflags: ["-Xep:DepAnn:ERROR"], @@ -104,8 +92,7 @@ java_library_static { name: "android.test.base-minus-junit", srcs: [ - "src/android/**/*.java", - "src/com/**/*.java", + "src/**/*.java", ], sdk_version: "current", diff --git a/test-base/hiddenapi/Android.bp b/test-base/hiddenapi/Android.bp index 1466590ef311..4c59b10ba423 100644 --- a/test-base/hiddenapi/Android.bp +++ b/test-base/hiddenapi/Android.bp @@ -15,12 +15,7 @@ // package { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } // Provided solely to contribute information about which hidden parts of the android.test.base diff --git a/test-junit/Android.bp b/test-junit/Android.bp new file mode 100644 index 000000000000..8d3d439e034e --- /dev/null +++ b/test-junit/Android.bp @@ -0,0 +1,53 @@ +// +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + default_applicable_licenses: ["frameworks-base-test-junit-license"], +} + +license { + name: "frameworks-base-test-junit-license", + visibility: [":__subpackages__"], + license_kinds: [ + "SPDX-license-identifier-CPL-1.0", + ], + license_text: [ + "src/junit/cpl-v10.html", + ], +} + +filegroup { + name: "frameworks-base-test-junit-framework", + srcs: [ + "src/junit/framework/**/*.java", + ], + path: "src", + visibility: [ + "//frameworks/base/test-base", + ], +} + +filegroup { + name: "frameworks-base-test-junit-runner", + srcs: [ + "src/junit/runner/**/*.java", + "src/junit/textui/**/*.java", + ], + path: "src", + visibility: [ + "//frameworks/base/test-runner", + ], +} diff --git a/test-base/src/junit/MODULE_LICENSE_CPL b/test-junit/src/junit/MODULE_LICENSE_CPL index e69de29bb2d1..e69de29bb2d1 100644 --- a/test-base/src/junit/MODULE_LICENSE_CPL +++ b/test-junit/src/junit/MODULE_LICENSE_CPL diff --git a/test-base/src/junit/README.android b/test-junit/src/junit/README.android index 1384a1fedda2..1384a1fedda2 100644 --- a/test-base/src/junit/README.android +++ b/test-junit/src/junit/README.android diff --git a/test-base/src/junit/cpl-v10.html b/test-junit/src/junit/cpl-v10.html index 36aa208d4a29..36aa208d4a29 100644 --- a/test-base/src/junit/cpl-v10.html +++ b/test-junit/src/junit/cpl-v10.html diff --git a/test-base/src/junit/framework/Assert.java b/test-junit/src/junit/framework/Assert.java index 3dcc23d71c19..3dcc23d71c19 100644 --- a/test-base/src/junit/framework/Assert.java +++ b/test-junit/src/junit/framework/Assert.java diff --git a/test-base/src/junit/framework/AssertionFailedError.java b/test-junit/src/junit/framework/AssertionFailedError.java index 0d7802c431c6..0d7802c431c6 100644 --- a/test-base/src/junit/framework/AssertionFailedError.java +++ b/test-junit/src/junit/framework/AssertionFailedError.java diff --git a/test-base/src/junit/framework/ComparisonCompactor.java b/test-junit/src/junit/framework/ComparisonCompactor.java index e540f03b87d3..e540f03b87d3 100644 --- a/test-base/src/junit/framework/ComparisonCompactor.java +++ b/test-junit/src/junit/framework/ComparisonCompactor.java diff --git a/test-base/src/junit/framework/ComparisonFailure.java b/test-junit/src/junit/framework/ComparisonFailure.java index 507799328a44..507799328a44 100644 --- a/test-base/src/junit/framework/ComparisonFailure.java +++ b/test-junit/src/junit/framework/ComparisonFailure.java diff --git a/test-base/src/junit/framework/Protectable.java b/test-junit/src/junit/framework/Protectable.java index e1432370cfaf..e1432370cfaf 100644 --- a/test-base/src/junit/framework/Protectable.java +++ b/test-junit/src/junit/framework/Protectable.java diff --git a/test-base/src/junit/framework/Test.java b/test-junit/src/junit/framework/Test.java index a016ee8308f1..a016ee8308f1 100644 --- a/test-base/src/junit/framework/Test.java +++ b/test-junit/src/junit/framework/Test.java diff --git a/test-base/src/junit/framework/TestCase.java b/test-junit/src/junit/framework/TestCase.java index b047ec9e1afc..b047ec9e1afc 100644 --- a/test-base/src/junit/framework/TestCase.java +++ b/test-junit/src/junit/framework/TestCase.java diff --git a/test-base/src/junit/framework/TestFailure.java b/test-junit/src/junit/framework/TestFailure.java index 6662b1fab1b2..6662b1fab1b2 100644 --- a/test-base/src/junit/framework/TestFailure.java +++ b/test-junit/src/junit/framework/TestFailure.java diff --git a/test-base/src/junit/framework/TestListener.java b/test-junit/src/junit/framework/TestListener.java index 9b6944361b9d..9b6944361b9d 100644 --- a/test-base/src/junit/framework/TestListener.java +++ b/test-junit/src/junit/framework/TestListener.java diff --git a/test-base/src/junit/framework/TestResult.java b/test-junit/src/junit/framework/TestResult.java index 3052e94074fd..3052e94074fd 100644 --- a/test-base/src/junit/framework/TestResult.java +++ b/test-junit/src/junit/framework/TestResult.java diff --git a/test-base/src/junit/framework/TestSuite.java b/test-junit/src/junit/framework/TestSuite.java index 336efd1800d7..336efd1800d7 100644 --- a/test-base/src/junit/framework/TestSuite.java +++ b/test-junit/src/junit/framework/TestSuite.java diff --git a/test-runner/src/junit/runner/BaseTestRunner.java b/test-junit/src/junit/runner/BaseTestRunner.java index b2fa16c91da2..b2fa16c91da2 100644 --- a/test-runner/src/junit/runner/BaseTestRunner.java +++ b/test-junit/src/junit/runner/BaseTestRunner.java diff --git a/test-runner/src/junit/runner/StandardTestSuiteLoader.java b/test-junit/src/junit/runner/StandardTestSuiteLoader.java index 808963a5aea0..808963a5aea0 100644 --- a/test-runner/src/junit/runner/StandardTestSuiteLoader.java +++ b/test-junit/src/junit/runner/StandardTestSuiteLoader.java diff --git a/test-runner/src/junit/runner/TestRunListener.java b/test-junit/src/junit/runner/TestRunListener.java index 0e9581989eee..0e9581989eee 100644 --- a/test-runner/src/junit/runner/TestRunListener.java +++ b/test-junit/src/junit/runner/TestRunListener.java diff --git a/test-runner/src/junit/runner/TestSuiteLoader.java b/test-junit/src/junit/runner/TestSuiteLoader.java index 9cc6d81e125e..9cc6d81e125e 100644 --- a/test-runner/src/junit/runner/TestSuiteLoader.java +++ b/test-junit/src/junit/runner/TestSuiteLoader.java diff --git a/test-runner/src/junit/runner/Version.java b/test-junit/src/junit/runner/Version.java index dd88c03372c8..dd88c03372c8 100644 --- a/test-runner/src/junit/runner/Version.java +++ b/test-junit/src/junit/runner/Version.java diff --git a/test-runner/src/junit/runner/package-info.java b/test-junit/src/junit/runner/package-info.java index 364e3621456e..364e3621456e 100644 --- a/test-runner/src/junit/runner/package-info.java +++ b/test-junit/src/junit/runner/package-info.java diff --git a/test-runner/src/junit/textui/ResultPrinter.java b/test-junit/src/junit/textui/ResultPrinter.java index b4914529bf4f..b4914529bf4f 100644 --- a/test-runner/src/junit/textui/ResultPrinter.java +++ b/test-junit/src/junit/textui/ResultPrinter.java diff --git a/test-runner/src/junit/textui/TestRunner.java b/test-junit/src/junit/textui/TestRunner.java index 046448e5e76a..046448e5e76a 100644 --- a/test-runner/src/junit/textui/TestRunner.java +++ b/test-junit/src/junit/textui/TestRunner.java diff --git a/test-runner/src/junit/textui/package-info.java b/test-junit/src/junit/textui/package-info.java index 28b2ef46b582..28b2ef46b582 100644 --- a/test-runner/src/junit/textui/package-info.java +++ b/test-junit/src/junit/textui/package-info.java diff --git a/test-mock/Android.bp b/test-mock/Android.bp index f37d2d17973e..e29d321e5105 100644 --- a/test-mock/Android.bp +++ b/test-mock/Android.bp @@ -17,12 +17,7 @@ // Build the android.test.mock library // =================================== package { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } java_sdk_library { diff --git a/test-runner/Android.bp b/test-runner/Android.bp index 21e09d3221ce..6b5be3cba204 100644 --- a/test-runner/Android.bp +++ b/test-runner/Android.bp @@ -14,29 +14,19 @@ // limitations under the License. // -// Build the android.test.runner library -// ===================================== package { - // See: http://go/android-license-faq - default_applicable_licenses: ["frameworks_base_test-runner_license"], -} - -license { - name: "frameworks_base_test-runner_license", - visibility: [":__subpackages__"], - license_kinds: [ - "SPDX-license-identifier-Apache-2.0", - "SPDX-license-identifier-CPL-1.0", - ], - license_text: [ - "src/junit/cpl-v10.html", - ], + default_applicable_licenses: ["Android-Apache-2.0"], } +// Build the android.test.runner library +// ===================================== java_sdk_library { name: "android.test.runner", - srcs: [":android-test-runner-sources"], + srcs: [ + ":android-test-runner-sources", + ":frameworks-base-test-junit-runner", + ], errorprone: { javacflags: ["-Xep:DepAnn:ERROR"], diff --git a/test-runner/src/junit/MODULE_LICENSE_CPL b/test-runner/src/junit/MODULE_LICENSE_CPL deleted file mode 100644 index e69de29bb2d1..000000000000 --- a/test-runner/src/junit/MODULE_LICENSE_CPL +++ /dev/null diff --git a/test-runner/src/junit/README.android b/test-runner/src/junit/README.android deleted file mode 100644 index 1384a1fedda2..000000000000 --- a/test-runner/src/junit/README.android +++ /dev/null @@ -1,11 +0,0 @@ -URL: https://github.com/junit-team/junit4 -License: Common Public License Version 1.0 -License File: cpl-v10.html - -This is JUnit 4.10 source that was previously part of the Android Public API. -Where necessary it has been patched to be compatible (according to Android API -requirements) with JUnit 3.8. - -These are copied here to ensure that the android.test.runner target remains -compatible with the last version of the Android API (25) that contained these -classes even when external/junit is upgraded to a later version. diff --git a/test-runner/src/junit/cpl-v10.html b/test-runner/src/junit/cpl-v10.html deleted file mode 100644 index 36aa208d4a29..000000000000 --- a/test-runner/src/junit/cpl-v10.html +++ /dev/null @@ -1,125 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN"> -<HTML> -<HEAD> -<TITLE>Common Public License - v 1.0</TITLE> -<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> -</HEAD> - -<BODY BGCOLOR="#FFFFFF" VLINK="#800000"> - - -<P ALIGN="CENTER"><B>Common Public License - v 1.0</B> -<P><B></B><FONT SIZE="3"></FONT> -<P><FONT SIZE="3"></FONT><FONT SIZE="2">THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.</FONT> -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2"><B>1. DEFINITIONS</B></FONT> -<P><FONT SIZE="2">"Contribution" means:</FONT> - -<UL><FONT SIZE="2">a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and<BR CLEAR="LEFT"> -b) in the case of each subsequent Contributor:</FONT></UL> - - -<UL><FONT SIZE="2">i) changes to the Program, and</FONT></UL> - - -<UL><FONT SIZE="2">ii) additions to the Program;</FONT></UL> - - -<UL><FONT SIZE="2">where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. </FONT><FONT SIZE="2">A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. </FONT><FONT SIZE="2">Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. </FONT></UL> - -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2">"Contributor" means any person or entity that distributes the Program.</FONT> -<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT> -<P><FONT SIZE="2">"Licensed Patents " mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. </FONT> -<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT> -<P><FONT SIZE="2"></FONT><FONT SIZE="2">"Program" means the Contributions distributed in accordance with this Agreement.</FONT> -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2">"Recipient" means anyone who receives the Program under this Agreement, including all Contributors.</FONT> -<P><FONT SIZE="2"><B></B></FONT> -<P><FONT SIZE="2"><B>2. GRANT OF RIGHTS</B></FONT> - -<UL><FONT SIZE="2"></FONT><FONT SIZE="2">a) </FONT><FONT SIZE="2">Subject to the terms of this Agreement, each Contributor hereby grants</FONT><FONT SIZE="2"> Recipient a non-exclusive, worldwide, royalty-free copyright license to</FONT><FONT SIZE="2" COLOR="#FF0000"> </FONT><FONT SIZE="2">reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form.</FONT></UL> - - -<UL><FONT SIZE="2"></FONT></UL> - - -<UL><FONT SIZE="2"></FONT><FONT SIZE="2">b) Subject to the terms of this Agreement, each Contributor hereby grants </FONT><FONT SIZE="2">Recipient a non-exclusive, worldwide,</FONT><FONT SIZE="2" COLOR="#008000"> </FONT><FONT SIZE="2">royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. </FONT></UL> - - -<UL><FONT SIZE="2"></FONT></UL> - - -<UL><FONT SIZE="2">c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program.</FONT></UL> - - -<UL><FONT SIZE="2"></FONT></UL> - - -<UL><FONT SIZE="2">d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. </FONT></UL> - - -<UL><FONT SIZE="2"></FONT></UL> - -<P><FONT SIZE="2"><B>3. REQUIREMENTS</B></FONT> -<P><FONT SIZE="2"><B></B>A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that:</FONT> - -<UL><FONT SIZE="2">a) it complies with the terms and conditions of this Agreement; and</FONT></UL> - - -<UL><FONT SIZE="2">b) its license agreement:</FONT></UL> - - -<UL><FONT SIZE="2">i) effectively disclaims</FONT><FONT SIZE="2"> on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; </FONT></UL> - - -<UL><FONT SIZE="2">ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; </FONT></UL> - - -<UL><FONT SIZE="2">iii)</FONT><FONT SIZE="2"> states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and</FONT></UL> - - -<UL><FONT SIZE="2">iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange.</FONT><FONT SIZE="2" COLOR="#0000FF"> </FONT><FONT SIZE="2" COLOR="#FF0000"></FONT></UL> - - -<UL><FONT SIZE="2" COLOR="#FF0000"></FONT><FONT SIZE="2"></FONT></UL> - -<P><FONT SIZE="2">When the Program is made available in source code form:</FONT> - -<UL><FONT SIZE="2">a) it must be made available under this Agreement; and </FONT></UL> - - -<UL><FONT SIZE="2">b) a copy of this Agreement must be included with each copy of the Program. </FONT></UL> - -<P><FONT SIZE="2"></FONT><FONT SIZE="2" COLOR="#0000FF"><STRIKE></STRIKE></FONT> -<P><FONT SIZE="2" COLOR="#0000FF"><STRIKE></STRIKE></FONT><FONT SIZE="2">Contributors may not remove or alter any copyright notices contained within the Program. </FONT> -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2">Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. </FONT> -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2"><B>4. COMMERCIAL DISTRIBUTION</B></FONT> -<P><FONT SIZE="2">Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense.</FONT> -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2">For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages.</FONT> -<P><FONT SIZE="2"></FONT><FONT SIZE="2" COLOR="#0000FF"></FONT> -<P><FONT SIZE="2" COLOR="#0000FF"></FONT><FONT SIZE="2"><B>5. NO WARRANTY</B></FONT> -<P><FONT SIZE="2">EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is</FONT><FONT SIZE="2"> solely responsible for determining the appropriateness of using and distributing </FONT><FONT SIZE="2">the Program</FONT><FONT SIZE="2"> and assumes all risks associated with its exercise of rights under this Agreement</FONT><FONT SIZE="2">, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, </FONT><FONT SIZE="2">programs or equipment, and unavailability or interruption of operations</FONT><FONT SIZE="2">. </FONT><FONT SIZE="2"></FONT> -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2"></FONT><FONT SIZE="2"><B>6. DISCLAIMER OF LIABILITY</B></FONT> -<P><FONT SIZE="2"></FONT><FONT SIZE="2">EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES </FONT><FONT SIZE="2">(INCLUDING WITHOUT LIMITATION LOST PROFITS),</FONT><FONT SIZE="2"> HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</FONT> -<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT> -<P><FONT SIZE="2"><B>7. GENERAL</B></FONT> -<P><FONT SIZE="2"></FONT><FONT SIZE="2">If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable.</FONT> -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2">If Recipient institutes patent litigation against a Contributor with respect to a patent applicable to software (including a cross-claim or counterclaim in a lawsuit), then any patent licenses granted by that Contributor to such Recipient under this Agreement shall terminate as of the date such litigation is filed. In addition, if Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. </FONT><FONT SIZE="2"></FONT> -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2">All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. </FONT><FONT SIZE="2"></FONT> -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2"></FONT><FONT SIZE="2">Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to </FONT><FONT SIZE="2">publish new versions (including revisions) of this Agreement from time to </FONT><FONT SIZE="2">time. No one other than the Agreement Steward has the right to modify this Agreement. IBM is the initial Agreement Steward. IBM may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. </FONT><FONT SIZE="2">Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new </FONT><FONT SIZE="2">version. </FONT><FONT SIZE="2">Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, </FONT><FONT SIZE="2">by implication, estoppel or otherwise</FONT><FONT SIZE="2">.</FONT><FONT SIZE="2"> All rights in the Program not expressly granted under this Agreement are reserved.</FONT> -<P><FONT SIZE="2"></FONT> -<P><FONT SIZE="2">This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation.</FONT> -<P><FONT SIZE="2"></FONT><FONT SIZE="2"></FONT> -<P><FONT SIZE="2"></FONT> - -</BODY> - -</HTML>
\ No newline at end of file diff --git a/test-runner/tests/Android.bp b/test-runner/tests/Android.bp index ac21bcb9d124..aad2bee8cb84 100644 --- a/test-runner/tests/Android.bp +++ b/test-runner/tests/Android.bp @@ -13,12 +13,7 @@ // limitations under the License. package { - // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: ["Android-Apache-2.0"], } android_test { diff --git a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java index 5460e4e87e2f..64dbe719311a 100644 --- a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java +++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerTest.java @@ -43,6 +43,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -392,6 +393,7 @@ public class AttachedChoreographerTest { } @Test + @Ignore("Can be enabled only after b/330536267 is ready") public void testChoreographerDivisorRefreshRate() { for (int divisor : new int[]{2, 3}) { CountDownLatch continueLatch = new CountDownLatch(1); @@ -420,6 +422,7 @@ public class AttachedChoreographerTest { } @Test + @Ignore("Can be enabled only after b/330536267 is ready") public void testChoreographerAttachedAfterSetFrameRate() { Log.i(TAG, "starting testChoreographerAttachedAfterSetFrameRate"); diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java index caaee634c57a..4d4827676c74 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java @@ -30,10 +30,12 @@ import com.android.compatibility.common.util.DisplayUtil; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +@Ignore // b/330376055: Write tests for functionality for both dVRR and MRR devices. @RunWith(AndroidJUnit4.class) public class SurfaceControlTest { private static final String TAG = "SurfaceControlTest"; diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt index 0c60f284a35b..ffed4087acff 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/GameAppHelper.kt @@ -47,7 +47,7 @@ constructor( val bound = gameView.getVisibleBounds() return uiDevice.swipe( bound.centerX(), - bound.top, + 0, bound.centerX(), bound.centerY(), SWIPE_STEPS diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp index e0e6c4c43b16..2c5fdd3228ed 100644 --- a/tests/PackageWatchdog/Android.bp +++ b/tests/PackageWatchdog/Android.bp @@ -28,8 +28,10 @@ android_test { static_libs: [ "junit", "mockito-target-extended-minus-junit4", + "flag-junit", "frameworks-base-testutils", "androidx.test.rules", + "PlatformProperties", "services.core", "services.net", "truth", diff --git a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java new file mode 100644 index 000000000000..081da11f2aa8 --- /dev/null +++ b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java @@ -0,0 +1,644 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.content.rollback.PackageRollbackInfo; +import android.content.rollback.RollbackInfo; +import android.content.rollback.RollbackManager; +import android.crashrecovery.flags.Flags; +import android.net.ConnectivityModuleConnector; +import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener; +import android.os.Handler; +import android.os.SystemProperties; +import android.os.test.TestLooper; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.DeviceConfig; +import android.util.AtomicFile; + +import androidx.test.InstrumentationRegistry; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.server.RescueParty.RescuePartyObserver; +import com.android.server.pm.ApexManager; +import com.android.server.rollback.RollbackPackageHealthObserver; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * Test CrashRecovery, integration tests that include PackageWatchdog, RescueParty and + * RollbackPackageHealthObserver + */ +public class CrashRecoveryTest { + private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG = + "persist.device_config.configuration.disable_rescue_party"; + + private static final String APP_A = "com.package.a"; + private static final String APP_B = "com.package.b"; + private static final String APP_C = "com.package.c"; + private static final long VERSION_CODE = 1L; + private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1); + + private static final RollbackInfo ROLLBACK_INFO_LOW = getRollbackInfo(APP_A, VERSION_CODE, 1, + PackageManager.ROLLBACK_USER_IMPACT_LOW); + private static final RollbackInfo ROLLBACK_INFO_HIGH = getRollbackInfo(APP_B, VERSION_CODE, 2, + PackageManager.ROLLBACK_USER_IMPACT_HIGH); + private static final RollbackInfo ROLLBACK_INFO_MANUAL = getRollbackInfo(APP_C, VERSION_CODE, 3, + PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private final TestClock mTestClock = new TestClock(); + private TestLooper mTestLooper; + private Context mSpyContext; + // Keep track of all created watchdogs to apply device config changes + private List<PackageWatchdog> mAllocatedWatchdogs; + @Mock + private ConnectivityModuleConnector mConnectivityModuleConnector; + @Mock + private PackageManager mMockPackageManager; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ApexManager mApexManager; + @Mock + RollbackManager mRollbackManager; + // Mock only sysprop apis + private PackageWatchdog.BootThreshold mSpyBootThreshold; + @Captor + private ArgumentCaptor<ConnectivityModuleHealthListener> mConnectivityModuleCallbackCaptor; + private MockitoSession mSession; + private HashMap<String, String> mSystemSettingsMap; + private HashMap<String, String> mCrashRecoveryPropertiesMap; + + @Before + public void setUp() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + MockitoAnnotations.initMocks(this); + new File(InstrumentationRegistry.getContext().getFilesDir(), + "package-watchdog.xml").delete(); + adoptShellPermissions(Manifest.permission.READ_DEVICE_CONFIG, + Manifest.permission.WRITE_DEVICE_CONFIG); + mTestLooper = new TestLooper(); + mSpyContext = spy(InstrumentationRegistry.getContext()); + when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager); + when(mMockPackageManager.getPackageInfo(anyString(), anyInt())).then(inv -> { + final PackageInfo res = new PackageInfo(); + res.packageName = inv.getArgument(0); + res.setLongVersionCode(VERSION_CODE); + return res; + }); + mSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .spyStatic(SystemProperties.class) + .spyStatic(RescueParty.class) + .startMocking(); + mSystemSettingsMap = new HashMap<>(); + + // Mock SystemProperties setter and various getters + doAnswer((Answer<Void>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + String value = invocationOnMock.getArgument(1); + + mSystemSettingsMap.put(key, value); + return null; + } + ).when(() -> SystemProperties.set(anyString(), anyString())); + + doAnswer((Answer<Integer>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + int defaultValue = invocationOnMock.getArgument(1); + + String storedValue = mSystemSettingsMap.get(key); + return storedValue == null ? defaultValue : Integer.parseInt(storedValue); + } + ).when(() -> SystemProperties.getInt(anyString(), anyInt())); + + doAnswer((Answer<Long>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + long defaultValue = invocationOnMock.getArgument(1); + + String storedValue = mSystemSettingsMap.get(key); + return storedValue == null ? defaultValue : Long.parseLong(storedValue); + } + ).when(() -> SystemProperties.getLong(anyString(), anyLong())); + + doAnswer((Answer<Boolean>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + boolean defaultValue = invocationOnMock.getArgument(1); + + String storedValue = mSystemSettingsMap.get(key); + return storedValue == null ? defaultValue : Boolean.parseBoolean(storedValue); + } + ).when(() -> SystemProperties.getBoolean(anyString(), anyBoolean())); + + SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(true)); + SystemProperties.set(PROP_DEVICE_CONFIG_DISABLE_FLAG, Boolean.toString(false)); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED, + Boolean.toString(true), false); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, + Integer.toString(PackageWatchdog.DEFAULT_TRIGGER_FAILURE_COUNT), false); + + mAllocatedWatchdogs = new ArrayList<>(); + RescuePartyObserver.reset(); + } + + @After + public void tearDown() throws Exception { + dropShellPermissions(); + mSession.finishMocking(); + // Clean up listeners since too many listeners will delay notifications significantly + for (PackageWatchdog watchdog : mAllocatedWatchdogs) { + watchdog.removePropertyChangedListener(); + } + mAllocatedWatchdogs.clear(); + } + + @Test + public void testBootLoopWithRescueParty() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog); + + verify(rescuePartyObserver, never()).executeBootLoopMitigation(1); + int bootCounter = 0; + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rescuePartyObserver).executeBootLoopMitigation(1); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rescuePartyObserver).executeBootLoopMitigation(2); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); + + int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter; + for (int i = 0; i < bootLoopThreshold; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(3); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(4); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(4); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(5); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(5); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(6); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(6); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(7); + } + + @Test + public void testBootLoopWithRollbackPackageHealthObserver() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + RollbackPackageHealthObserver rollbackObserver = + setUpRollbackPackageHealthObserver(watchdog); + + verify(rollbackObserver, never()).executeBootLoopMitigation(1); + int bootCounter = 0; + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rollbackObserver).executeBootLoopMitigation(1); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + + // Update the list of available rollbacks after executing bootloop mitigation once + when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH, + ROLLBACK_INFO_MANUAL)); + + int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter; + for (int i = 0; i < bootLoopThreshold; i++) { + watchdog.noteBoot(); + } + verify(rollbackObserver).executeBootLoopMitigation(2); + verify(rollbackObserver, never()).executeBootLoopMitigation(3); + + // Update the list of available rollbacks after executing bootloop mitigation once + when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL)); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rollbackObserver, never()).executeBootLoopMitigation(3); + } + + @Test + public void testBootLoopWithRescuePartyAndRollbackPackageHealthObserver() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog); + RollbackPackageHealthObserver rollbackObserver = + setUpRollbackPackageHealthObserver(watchdog); + + verify(rescuePartyObserver, never()).executeBootLoopMitigation(1); + verify(rollbackObserver, never()).executeBootLoopMitigation(1); + int bootCounter = 0; + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rescuePartyObserver).executeBootLoopMitigation(1); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); + verify(rollbackObserver, never()).executeBootLoopMitigation(1); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rescuePartyObserver).executeBootLoopMitigation(2); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); + verify(rollbackObserver).executeBootLoopMitigation(1); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + // Update the list of available rollbacks after executing bootloop mitigation once + when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH, + ROLLBACK_INFO_MANUAL)); + + int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter; + for (int i = 0; i < bootLoopThreshold; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(3); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(4); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(4); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(5); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(5); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(6); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver, never()).executeBootLoopMitigation(6); + verify(rollbackObserver).executeBootLoopMitigation(2); + verify(rollbackObserver, never()).executeBootLoopMitigation(3); + // Update the list of available rollbacks after executing bootloop mitigation + when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL)); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(6); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(7); + verify(rollbackObserver, never()).executeBootLoopMitigation(3); + } + + RollbackPackageHealthObserver setUpRollbackPackageHealthObserver(PackageWatchdog watchdog) { + RollbackPackageHealthObserver rollbackObserver = + spy(new RollbackPackageHealthObserver(mSpyContext, mApexManager)); + when(mSpyContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager); + when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_LOW, + ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL)); + when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager); + + watchdog.registerHealthObserver(rollbackObserver); + return rollbackObserver; + } + + RescuePartyObserver setUpRescuePartyObserver(PackageWatchdog watchdog) { + setCrashRecoveryPropRescueBootCount(0); + RescuePartyObserver rescuePartyObserver = spy(RescuePartyObserver.getInstance(mSpyContext)); + assertFalse(RescueParty.isRebootPropertySet()); + watchdog.registerHealthObserver(rescuePartyObserver); + return rescuePartyObserver; + } + + private static RollbackInfo getRollbackInfo(String packageName, long versionCode, + int rollbackId, int rollbackUserImpact) { + VersionedPackage appFrom = new VersionedPackage(packageName, versionCode + 1); + VersionedPackage appTo = new VersionedPackage(packageName, versionCode); + PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appFrom, appTo, null, + null, false, false, null); + RollbackInfo rollbackInfo = new RollbackInfo(rollbackId, List.of(packageRollbackInfo), + false, null, 111, rollbackUserImpact); + return rollbackInfo; + } + + private void adoptShellPermissions(String... permissions) { + androidx.test.platform.app.InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(permissions); + } + + private void dropShellPermissions() { + androidx.test.platform.app.InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + + + private PackageWatchdog createWatchdog() { + return createWatchdog(new TestController(), true /* withPackagesReady */); + } + + private PackageWatchdog createWatchdog(TestController controller, boolean withPackagesReady) { + AtomicFile policyFile = + new AtomicFile(new File(mSpyContext.getFilesDir(), "package-watchdog.xml")); + Handler handler = new Handler(mTestLooper.getLooper()); + PackageWatchdog watchdog = + new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller, + mConnectivityModuleConnector, mTestClock); + mockCrashRecoveryProperties(watchdog); + + // Verify controller is not automatically started + assertThat(controller.mIsEnabled).isFalse(); + if (withPackagesReady) { + // Only capture the NetworkStack callback for the latest registered watchdog + reset(mConnectivityModuleConnector); + watchdog.onPackagesReady(); + // Verify controller by default is started when packages are ready + assertThat(controller.mIsEnabled).isTrue(); + + verify(mConnectivityModuleConnector).registerHealthListener( + mConnectivityModuleCallbackCaptor.capture()); + } + mAllocatedWatchdogs.add(watchdog); + return watchdog; + } + + // Mock CrashRecoveryProperties as they cannot be accessed due to SEPolicy restrictions + private void mockCrashRecoveryProperties(PackageWatchdog watchdog) { + mCrashRecoveryPropertiesMap = new HashMap<>(); + + // mock properties in RescueParty + try { + + doAnswer((Answer<Boolean>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.attempting_factory_reset", "false"); + return Boolean.parseBoolean(storedValue); + }).when(() -> RescueParty.isFactoryResetPropertySet()); + doAnswer((Answer<Void>) invocationOnMock -> { + boolean value = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.attempting_factory_reset", + Boolean.toString(value)); + return null; + }).when(() -> RescueParty.setFactoryResetProperty(anyBoolean())); + + doAnswer((Answer<Boolean>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.attempting_reboot", "false"); + return Boolean.parseBoolean(storedValue); + }).when(() -> RescueParty.isRebootPropertySet()); + doAnswer((Answer<Void>) invocationOnMock -> { + boolean value = invocationOnMock.getArgument(0); + setCrashRecoveryPropAttemptingReboot(value); + return null; + }).when(() -> RescueParty.setRebootProperty(anyBoolean())); + + doAnswer((Answer<Long>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("persist.crashrecovery.last_factory_reset", "0"); + return Long.parseLong(storedValue); + }).when(() -> RescueParty.getLastFactoryResetTimeMs()); + doAnswer((Answer<Void>) invocationOnMock -> { + long value = invocationOnMock.getArgument(0); + setCrashRecoveryPropLastFactoryReset(value); + return null; + }).when(() -> RescueParty.setLastFactoryResetTimeMs(anyLong())); + + doAnswer((Answer<Integer>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.max_rescue_level_attempted", "0"); + return Integer.parseInt(storedValue); + }).when(() -> RescueParty.getMaxRescueLevelAttempted()); + doAnswer((Answer<Void>) invocationOnMock -> { + int value = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.max_rescue_level_attempted", + Integer.toString(value)); + return null; + }).when(() -> RescueParty.setMaxRescueLevelAttempted(anyInt())); + + } catch (Exception e) { + // tests will fail, just printing the error + System.out.println("Error while mocking crashrecovery properties " + e.getMessage()); + } + + try { + if (Flags.recoverabilityDetection()) { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, + PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + } else { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); + } + + doAnswer((Answer<Integer>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.rescue_boot_count", "0"); + return Integer.parseInt(storedValue); + }).when(mSpyBootThreshold).getCount(); + doAnswer((Answer<Void>) invocationOnMock -> { + int count = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_count", + Integer.toString(count)); + return null; + }).when(mSpyBootThreshold).setCount(anyInt()); + + doAnswer((Answer<Integer>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.boot_mitigation_count", "0"); + return Integer.parseInt(storedValue); + }).when(mSpyBootThreshold).getMitigationCount(); + doAnswer((Answer<Void>) invocationOnMock -> { + int count = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.boot_mitigation_count", + Integer.toString(count)); + return null; + }).when(mSpyBootThreshold).setMitigationCount(anyInt()); + + doAnswer((Answer<Long>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.rescue_boot_start", "0"); + return Long.parseLong(storedValue); + }).when(mSpyBootThreshold).getStart(); + doAnswer((Answer<Void>) invocationOnMock -> { + long count = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_start", + Long.toString(count)); + return null; + }).when(mSpyBootThreshold).setStart(anyLong()); + + doAnswer((Answer<Long>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.boot_mitigation_start", "0"); + return Long.parseLong(storedValue); + }).when(mSpyBootThreshold).getMitigationStart(); + doAnswer((Answer<Void>) invocationOnMock -> { + long count = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.boot_mitigation_start", + Long.toString(count)); + return null; + }).when(mSpyBootThreshold).setMitigationStart(anyLong()); + + Field mBootThresholdField = watchdog.getClass().getDeclaredField("mBootThreshold"); + mBootThresholdField.setAccessible(true); + mBootThresholdField.set(watchdog, mSpyBootThreshold); + } catch (Exception e) { + // tests will fail, just printing the error + System.out.println("Error detected while spying BootThreshold" + e.getMessage()); + } + } + + private void setCrashRecoveryPropRescueBootCount(int count) { + mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_count", + Integer.toString(count)); + } + + private void setCrashRecoveryPropAttemptingReboot(boolean value) { + mCrashRecoveryPropertiesMap.put("crashrecovery.attempting_reboot", + Boolean.toString(value)); + } + + private void setCrashRecoveryPropLastFactoryReset(long value) { + mCrashRecoveryPropertiesMap.put("persist.crashrecovery.last_factory_reset", + Long.toString(value)); + } + + private static class TestController extends ExplicitHealthCheckController { + TestController() { + super(null /* controller */); + } + + private boolean mIsEnabled; + private List<String> mSupportedPackages = new ArrayList<>(); + private List<String> mRequestedPackages = new ArrayList<>(); + private Consumer<List<PackageConfig>> mSupportedConsumer; + private List<Set> mSyncRequests = new ArrayList<>(); + + @Override + public void setEnabled(boolean enabled) { + mIsEnabled = enabled; + if (!mIsEnabled) { + mSupportedPackages.clear(); + } + } + + @Override + public void setCallbacks(Consumer<String> passedConsumer, + Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) { + mSupportedConsumer = supportedConsumer; + } + + @Override + public void syncRequests(Set<String> packages) { + mSyncRequests.add(packages); + mRequestedPackages.clear(); + if (mIsEnabled) { + packages.retainAll(mSupportedPackages); + mRequestedPackages.addAll(packages); + List<PackageConfig> packageConfigs = new ArrayList<>(); + for (String packageName: packages) { + packageConfigs.add(new PackageConfig(packageName, SHORT_DURATION)); + } + mSupportedConsumer.accept(packageConfigs); + } else { + mSupportedConsumer.accept(Collections.emptyList()); + } + } + } + + private static class TestClock implements PackageWatchdog.SystemClock { + // Note 0 is special to the internal clock of PackageWatchdog. We need to start from + // a non-zero value in order not to disrupt the logic of PackageWatchdog. + private long mUpTimeMillis = 1; + @Override + public long uptimeMillis() { + return mUpTimeMillis; + } + } +} diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index 75284c712bd2..4f27e06083ba 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -36,11 +36,13 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; import android.net.ConnectivityModuleConnector; import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener; import android.os.Handler; import android.os.SystemProperties; import android.os.test.TestLooper; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.util.AtomicFile; import android.util.Xml; @@ -54,11 +56,13 @@ import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.PackageWatchdog.HealthCheckState; import com.android.server.PackageWatchdog.MonitoredPackage; +import com.android.server.PackageWatchdog.ObserverInternal; import com.android.server.PackageWatchdog.PackageHealthObserver; import com.android.server.PackageWatchdog.PackageHealthObserverImpact; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; @@ -99,6 +103,10 @@ public class PackageWatchdogTest { private static final String OBSERVER_NAME_4 = "observer4"; private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1); private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final TestClock mTestClock = new TestClock(); private TestLooper mTestLooper; private Context mSpyContext; @@ -128,6 +136,7 @@ public class PackageWatchdogTest { @Before public void setUp() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); MockitoAnnotations.initMocks(this); new File(InstrumentationRegistry.getContext().getFilesDir(), "package-watchdog.xml").delete(); @@ -444,6 +453,7 @@ public class PackageWatchdogTest { */ @Test public void testPackageFailureNotifyAllDifferentImpacts() throws Exception { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver observerNone = new TestObserver(OBSERVER_NAME_1, PackageHealthObserverImpact.USER_IMPACT_LEVEL_0); @@ -488,6 +498,52 @@ public class PackageWatchdogTest { assertThat(observerLowPackages).containsExactly(APP_A); } + @Test + public void testPackageFailureNotifyAllDifferentImpactsRecoverability() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observerNone = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_0); + TestObserver observerHigh = new TestObserver(OBSERVER_NAME_2, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_50); + TestObserver observerMid = new TestObserver(OBSERVER_NAME_3, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); + TestObserver observerLow = new TestObserver(OBSERVER_NAME_4, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); + + // Start observing for all impact observers + watchdog.startObservingHealth(observerNone, Arrays.asList(APP_A, APP_B, APP_C, APP_D), + SHORT_DURATION); + watchdog.startObservingHealth(observerHigh, Arrays.asList(APP_A, APP_B, APP_C), + SHORT_DURATION); + watchdog.startObservingHealth(observerMid, Arrays.asList(APP_A, APP_B), + SHORT_DURATION); + watchdog.startObservingHealth(observerLow, Arrays.asList(APP_A), + SHORT_DURATION); + + // Then fail all apps above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE), + new VersionedPackage(APP_B, VERSION_CODE), + new VersionedPackage(APP_C, VERSION_CODE), + new VersionedPackage(APP_D, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify least impact observers are notifed of package failures + List<String> observerNonePackages = observerNone.mMitigatedPackages; + List<String> observerHighPackages = observerHigh.mMitigatedPackages; + List<String> observerMidPackages = observerMid.mMitigatedPackages; + List<String> observerLowPackages = observerLow.mMitigatedPackages; + + // APP_D failure observed by only observerNone is not caught cos its impact is none + assertThat(observerNonePackages).isEmpty(); + // APP_C failure is caught by observerHigh cos it's the lowest impact observer + assertThat(observerHighPackages).containsExactly(APP_C); + // APP_B failure is caught by observerMid cos it's the lowest impact observer + assertThat(observerMidPackages).containsExactly(APP_B); + // APP_A failure is caught by observerLow cos it's the lowest impact observer + assertThat(observerLowPackages).containsExactly(APP_A); + } + /** * Test package failure and least impact observers are notified successively. * State transistions: @@ -501,6 +557,7 @@ public class PackageWatchdogTest { */ @Test public void testPackageFailureNotifyLeastImpactSuccessively() throws Exception { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1, PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); @@ -563,11 +620,76 @@ public class PackageWatchdogTest { assertThat(observerSecond.mMitigatedPackages).isEmpty(); } + @Test + public void testPackageFailureNotifyLeastImpactSuccessivelyRecoverability() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); + TestObserver observerSecond = new TestObserver(OBSERVER_NAME_2, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); + + // Start observing for observerFirst and observerSecond with failure handling + watchdog.startObservingHealth(observerFirst, Arrays.asList(APP_A), LONG_DURATION); + watchdog.startObservingHealth(observerSecond, Arrays.asList(APP_A), LONG_DURATION); + + // Then fail APP_A above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify only observerFirst is notifed + assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A); + assertThat(observerSecond.mMitigatedPackages).isEmpty(); + + // After observerFirst handles failure, next action it has is high impact + observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; + observerFirst.mMitigatedPackages.clear(); + observerSecond.mMitigatedPackages.clear(); + + // Then fail APP_A again above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify only observerSecond is notifed cos it has least impact + assertThat(observerSecond.mMitigatedPackages).containsExactly(APP_A); + assertThat(observerFirst.mMitigatedPackages).isEmpty(); + + // After observerSecond handles failure, it has no further actions + observerSecond.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + observerFirst.mMitigatedPackages.clear(); + observerSecond.mMitigatedPackages.clear(); + + // Then fail APP_A again above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify only observerFirst is notifed cos it has the only action + assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A); + assertThat(observerSecond.mMitigatedPackages).isEmpty(); + + // After observerFirst handles failure, it too has no further actions + observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + observerFirst.mMitigatedPackages.clear(); + observerSecond.mMitigatedPackages.clear(); + + // Then fail APP_A again above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify no observer is notified cos no actions left + assertThat(observerFirst.mMitigatedPackages).isEmpty(); + assertThat(observerSecond.mMitigatedPackages).isEmpty(); + } + /** * Test package failure and notifies only one observer even with observer impact tie. */ @Test public void testPackageFailureNotifyOneSameImpact() throws Exception { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver observer1 = new TestObserver(OBSERVER_NAME_1, PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); @@ -588,6 +710,28 @@ public class PackageWatchdogTest { assertThat(observer2.mMitigatedPackages).isEmpty(); } + @Test + public void testPackageFailureNotifyOneSameImpactRecoverabilityDetection() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_50); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_50); + + // Start observing for observer1 and observer2 with failure handling + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + + // Then fail APP_A above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify only one observer is notifed + assertThat(observer1.mMitigatedPackages).containsExactly(APP_A); + assertThat(observer2.mMitigatedPackages).isEmpty(); + } + /** * Test package passing explicit health checks does not fail and vice versa. */ @@ -818,6 +962,7 @@ public class PackageWatchdogTest { @Test public void testNetworkStackFailure() { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); final PackageWatchdog wd = createWatchdog(); // Start observing with failure handling @@ -835,6 +980,25 @@ public class PackageWatchdogTest { assertThat(observer.mMitigatedPackages).containsExactly(APP_A); } + @Test + public void testNetworkStackFailureRecoverabilityDetection() { + final PackageWatchdog wd = createWatchdog(); + + // Start observing with failure handling + TestObserver observer = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); + wd.startObservingHealth(observer, Collections.singletonList(APP_A), SHORT_DURATION); + + // Notify of NetworkStack failure + mConnectivityModuleCallbackCaptor.getValue().onNetworkStackFailure(APP_A); + + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify the NetworkStack observer is notified + assertThat(observer.mMitigatedPackages).isEmpty(); + } + /** Test default values are used when device property is invalid. */ @Test public void testInvalidConfig_watchdogTriggerFailureCount() { @@ -1045,6 +1209,7 @@ public class PackageWatchdogTest { /** Ensure that boot loop mitigation is done when the number of boots meets the threshold. */ @Test public void testBootLoopDetection_meetsThreshold() { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); watchdog.registerHealthObserver(bootObserver); @@ -1054,6 +1219,16 @@ public class PackageWatchdogTest { assertThat(bootObserver.mitigatedBootLoop()).isTrue(); } + @Test + public void testBootLoopDetection_meetsThresholdRecoverability() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); + watchdog.registerHealthObserver(bootObserver); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) { + watchdog.noteBoot(); + } + assertThat(bootObserver.mitigatedBootLoop()).isTrue(); + } /** * Ensure that boot loop mitigation is not done when the number of boots does not meet the @@ -1071,10 +1246,43 @@ public class PackageWatchdogTest { } /** + * Ensure that boot loop mitigation is not done when the number of boots does not meet the + * threshold. + */ + @Test + public void testBootLoopDetection_doesNotMeetThresholdRecoverabilityLowImpact() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); + watchdog.registerHealthObserver(bootObserver); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; i++) { + watchdog.noteBoot(); + } + assertThat(bootObserver.mitigatedBootLoop()).isFalse(); + } + + /** + * Ensure that boot loop mitigation is not done when the number of boots does not meet the + * threshold. + */ + @Test + public void testBootLoopDetection_doesNotMeetThresholdRecoverabilityHighImpact() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_80); + watchdog.registerHealthObserver(bootObserver); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; i++) { + watchdog.noteBoot(); + } + assertThat(bootObserver.mitigatedBootLoop()).isFalse(); + } + + /** * Ensure that boot loop mitigation is done for the observer with the lowest user impact */ @Test public void testBootLoopMitigationDoneForLowestUserImpact() { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver1 = new TestObserver(OBSERVER_NAME_1); bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); @@ -1089,11 +1297,28 @@ public class PackageWatchdogTest { assertThat(bootObserver2.mitigatedBootLoop()).isFalse(); } + @Test + public void testBootLoopMitigationDoneForLowestUserImpactRecoverability() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver1 = new TestObserver(OBSERVER_NAME_1); + bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); + TestObserver bootObserver2 = new TestObserver(OBSERVER_NAME_2); + bootObserver2.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); + watchdog.registerHealthObserver(bootObserver1); + watchdog.registerHealthObserver(bootObserver2); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) { + watchdog.noteBoot(); + } + assertThat(bootObserver1.mitigatedBootLoop()).isTrue(); + assertThat(bootObserver2.mitigatedBootLoop()).isFalse(); + } + /** * Ensure that the correct mitigation counts are sent to the boot loop observer. */ @Test public void testMultipleBootLoopMitigation() { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); watchdog.registerHealthObserver(bootObserver); @@ -1114,6 +1339,64 @@ public class PackageWatchdogTest { assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4)); } + @Test + public void testMultipleBootLoopMitigationRecoverabilityLowImpact() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); + watchdog.registerHealthObserver(bootObserver); + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; j++) { + watchdog.noteBoot(); + } + for (int i = 0; i < 4; i++) { + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) { + watchdog.noteBoot(); + } + } + + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1); + + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; j++) { + watchdog.noteBoot(); + } + for (int i = 0; i < 4; i++) { + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) { + watchdog.noteBoot(); + } + } + + assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4)); + } + + @Test + public void testMultipleBootLoopMitigationRecoverabilityHighImpact() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_80); + watchdog.registerHealthObserver(bootObserver); + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; j++) { + watchdog.noteBoot(); + } + for (int i = 0; i < 4; i++) { + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) { + watchdog.noteBoot(); + } + } + + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1); + + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; j++) { + watchdog.noteBoot(); + } + for (int i = 0; i < 4; i++) { + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) { + watchdog.noteBoot(); + } + } + + assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4)); + } + /** * Ensure that passing a null list of failed packages does not cause any mitigation logic to * execute. @@ -1304,6 +1587,78 @@ public class PackageWatchdogTest { } /** + * Ensure that a {@link ObserverInternal} may be correctly written and read in order to persist + * across reboots. + */ + @Test + @SuppressWarnings("GuardedBy") + public void testWritingAndReadingObserverInternalRecoverability() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + + LongArrayQueue mitigationCalls = new LongArrayQueue(); + mitigationCalls.addLast(1000); + mitigationCalls.addLast(2000); + mitigationCalls.addLast(3000); + MonitoredPackage writePkg = watchdog.newMonitoredPackage( + "test.package", 1000, 2000, true, mitigationCalls); + final int bootMitigationCount = 4; + ObserverInternal writeObserver = new ObserverInternal("test", List.of(writePkg), + bootMitigationCount); + + // Write the observer + File tmpFile = File.createTempFile("observer-watchdog-test", ".xml"); + AtomicFile testFile = new AtomicFile(tmpFile); + FileOutputStream stream = testFile.startWrite(); + TypedXmlSerializer outputSerializer = Xml.resolveSerializer(stream); + outputSerializer.startDocument(null, true); + writeObserver.writeLocked(outputSerializer); + outputSerializer.endDocument(); + testFile.finishWrite(stream); + + // Read the observer + TypedXmlPullParser parser = Xml.resolvePullParser(testFile.openRead()); + XmlUtils.beginDocument(parser, "observer"); + ObserverInternal readObserver = ObserverInternal.read(parser, watchdog); + + assertThat(readObserver.name).isEqualTo(writeObserver.name); + assertThat(readObserver.getBootMitigationCount()).isEqualTo(bootMitigationCount); + } + + /** + * Ensure that boot mitigation counts may be correctly written and read as metadata + * in order to persist across reboots. + */ + @Test + @SuppressWarnings("GuardedBy") + public void testWritingAndReadingMetadataBootMitigationCountRecoverability() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + String filePath = InstrumentationRegistry.getContext().getFilesDir().toString() + + "metadata_file.txt"; + + ObserverInternal observer1 = new ObserverInternal("test1", List.of(), 1); + ObserverInternal observer2 = new ObserverInternal("test2", List.of(), 2); + watchdog.registerObserverInternal(observer1); + watchdog.registerObserverInternal(observer2); + + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, + PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + + watchdog.saveAllObserversBootMitigationCountToMetadata(filePath); + + observer1.setBootMitigationCount(0); + observer2.setBootMitigationCount(0); + assertThat(observer1.getBootMitigationCount()).isEqualTo(0); + assertThat(observer2.getBootMitigationCount()).isEqualTo(0); + + mSpyBootThreshold.readAllObserversBootMitigationCountIfNecessary(filePath); + + assertThat(observer1.getBootMitigationCount()).isEqualTo(1); + assertThat(observer2.getBootMitigationCount()).isEqualTo(2); + } + + /** * Tests device config changes are propagated correctly. */ @Test @@ -1440,11 +1795,19 @@ public class PackageWatchdogTest { // Mock CrashRecoveryProperties as they cannot be accessed due to SEPolicy restrictions private void mockCrashRecoveryProperties(PackageWatchdog watchdog) { + mCrashRecoveryPropertiesMap = new HashMap<>(); + try { - mSpyBootThreshold = spy(watchdog.new BootThreshold( - PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, - PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); - mCrashRecoveryPropertiesMap = new HashMap<>(); + if (Flags.recoverabilityDetection()) { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, + PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + } else { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); + } doAnswer((Answer<Integer>) invocationOnMock -> { String storedValue = mCrashRecoveryPropertiesMap diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java index 1d7be2f4f039..5107943c3528 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/IpSecPacketLossDetectorTest.java @@ -333,6 +333,7 @@ public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase { public void testHandleLossRate_validationFail() throws Exception { checkHandleLossRate( 22, true /* isLastStateExpectedToUpdate */, true /* isCallbackExpected */); + verify(mConnectivityManager).reportNetworkConnectivity(mNetwork, false); } @Test diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java index 381c57496878..444208edc473 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.content.Context; +import android.net.ConnectivityManager; import android.net.IpSecConfig; import android.net.IpSecTransform; import android.net.LinkProperties; @@ -33,12 +34,14 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.TelephonyNetworkSpecifier; import android.net.vcn.FeatureFlags; +import android.net.vcn.Flags; import android.os.Handler; import android.os.IPowerManager; import android.os.IThermalService; import android.os.ParcelUuid; import android.os.PowerManager; import android.os.test.TestLooper; +import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.TelephonyManager; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; @@ -46,6 +49,7 @@ import com.android.server.vcn.VcnContext; import com.android.server.vcn.VcnNetworkProvider; import org.junit.Before; +import org.junit.Rule; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -53,6 +57,8 @@ import java.util.Set; import java.util.UUID; public abstract class NetworkEvaluationTestBase { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + protected static final String SSID = "TestWifi"; protected static final String SSID_OTHER = "TestWifiOther"; protected static final String PLMN_ID = "123456"; @@ -103,6 +109,7 @@ public abstract class NetworkEvaluationTestBase { @Mock protected FeatureFlags mFeatureFlags; @Mock protected android.net.platform.flags.FeatureFlags mCoreNetFeatureFlags; @Mock protected TelephonySubscriptionSnapshot mSubscriptionSnapshot; + @Mock protected ConnectivityManager mConnectivityManager; @Mock protected TelephonyManager mTelephonyManager; @Mock protected IPowerManager mPowerManagerService; @@ -114,6 +121,8 @@ public abstract class NetworkEvaluationTestBase { public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mSetFlagsRule.enableFlags(Flags.FLAG_VALIDATE_NETWORK_ON_IPSEC_LOSS); + when(mNetwork.getNetId()).thenReturn(-1); mTestLooper = new TestLooper(); @@ -130,6 +139,12 @@ public abstract class NetworkEvaluationTestBase { doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled(); setupSystemService( + mContext, + mConnectivityManager, + Context.CONNECTIVITY_SERVICE, + ConnectivityManager.class); + + setupSystemService( mContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class); when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager); when(mTelephonyManager.getNetworkOperator()).thenReturn(PLMN_ID); diff --git a/tools/app_metadata_bundles/Android.bp b/tools/app_metadata_bundles/Android.bp new file mode 100644 index 000000000000..be6bea6b7fea --- /dev/null +++ b/tools/app_metadata_bundles/Android.bp @@ -0,0 +1,26 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_host { + name: "asllib", + srcs: [ + "src/lib/java/**/*.java", + ], +} + +java_binary_host { + name: "aslgen", + manifest: "src/aslgen/aslgen.mf", + srcs: [ + "src/aslgen/java/**/*.java", + ], + static_libs: [ + "asllib", + ], +} diff --git a/tools/app_metadata_bundles/OWNERS b/tools/app_metadata_bundles/OWNERS new file mode 100644 index 000000000000..a2a250b2d5b7 --- /dev/null +++ b/tools/app_metadata_bundles/OWNERS @@ -0,0 +1,2 @@ +wenhaowang@google.com +mloh@google.com diff --git a/tools/app_metadata_bundles/README.md b/tools/app_metadata_bundles/README.md new file mode 100644 index 000000000000..6e8d287b41dd --- /dev/null +++ b/tools/app_metadata_bundles/README.md @@ -0,0 +1,9 @@ +# App metadata bundles + +This project delivers a comprehensive toolchain solution for developers +to efficiently manage app metadata bundles. + +The project consists of two subprojects: + + * A pure Java library, and + * A pure Java command-line tool. diff --git a/tools/app_metadata_bundles/src/aslgen/aslgen.mf b/tools/app_metadata_bundles/src/aslgen/aslgen.mf new file mode 100644 index 000000000000..fc656e2155a7 --- /dev/null +++ b/tools/app_metadata_bundles/src/aslgen/aslgen.mf @@ -0,0 +1 @@ +Main-Class: com.android.aslgen.Main
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java new file mode 100644 index 000000000000..fb7a6ab42d95 --- /dev/null +++ b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.aslgen; + +import com.android.asllib.AndroidSafetyLabel; +import com.android.asllib.AndroidSafetyLabel.Format; +import com.android.asllib.util.MalformedXmlException; + +import org.xml.sax.SAXException; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +public class Main { + + /** Takes the options to make file conversion. */ + public static void main(String[] args) + throws IOException, + ParserConfigurationException, + SAXException, + TransformerException, + MalformedXmlException { + + String inFile = null; + String outFile = null; + Format inFormat = Format.NULL; + Format outFormat = Format.NULL; + + + // Except for "--help", all arguments require a value currently. + // So just make sure we have an even number and + // then process them all two at a time. + if (args.length == 1 && "--help".equals(args[0])) { + showUsage(); + return; + } + if (args.length % 2 != 0) { + throw new IllegalArgumentException("Argument is missing corresponding value"); + } + for (int i = 0; i < args.length - 1; i += 2) { + final String arg = args[i].trim(); + final String argValue = args[i + 1].trim(); + if ("--in-path".equals(arg)) { + inFile = argValue; + } else if ("--out-path".equals(arg)) { + outFile = argValue; + } else if ("--in-format".equals(arg)) { + inFormat = getFormat(argValue); + } else if ("--out-format".equals(arg)) { + outFormat = getFormat(argValue); + } else { + throw new IllegalArgumentException("Unknown argument: " + arg); + } + } + + if (inFile == null) { + throw new IllegalArgumentException("input file is required"); + } + + if (outFile == null) { + throw new IllegalArgumentException("output file is required"); + } + + if (inFormat == Format.NULL) { + throw new IllegalArgumentException("input format is required"); + } + + if (outFormat == Format.NULL) { + throw new IllegalArgumentException("output format is required"); + } + + System.out.println("in path: " + inFile); + System.out.println("out path: " + outFile); + System.out.println("in format: " + inFormat); + System.out.println("out format: " + outFormat); + + var asl = AndroidSafetyLabel.readFromStream(new FileInputStream(inFile), inFormat); + asl.writeToStream(new FileOutputStream(outFile), outFormat); + } + + private static Format getFormat(String argValue) { + if ("hr".equals(argValue)) { + return Format.HUMAN_READABLE; + } else if ("od".equals(argValue)) { + return Format.ON_DEVICE; + } else { + return Format.NULL; + } + } + + private static void showUsage() { + AndroidSafetyLabel.test(); + System.err.println( + "Usage:\n" + ); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java new file mode 100644 index 000000000000..bc8063ef7b5f --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib; + +import com.android.asllib.util.MalformedXmlException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +public class AndroidSafetyLabel implements AslMarshallable { + + public enum Format { + NULL, HUMAN_READABLE, ON_DEVICE; + } + + private final SafetyLabels mSafetyLabels; + + public SafetyLabels getSafetyLabels() { + return mSafetyLabels; + } + + public AndroidSafetyLabel(SafetyLabels safetyLabels) { + this.mSafetyLabels = safetyLabels; + } + + /** Reads a {@link AndroidSafetyLabel} from an {@link InputStream}. */ + // TODO(b/329902686): Support parsing from on-device. + public static AndroidSafetyLabel readFromStream(InputStream in, Format format) + throws IOException, ParserConfigurationException, SAXException, MalformedXmlException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + Document document = factory.newDocumentBuilder().parse(in); + + switch (format) { + case HUMAN_READABLE: + Element appMetadataBundles = + XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES); + + return new AndroidSafetyLabelFactory() + .createFromHrElements( + List.of( + XmlUtils.getSingleElement( + document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES))); + case ON_DEVICE: + throw new IllegalArgumentException( + "Parsing from on-device format is not supported at this time."); + default: + throw new IllegalStateException("Unrecognized input format."); + } + } + + /** Write the content of the {@link AndroidSafetyLabel} to a {@link OutputStream}. */ + // TODO(b/329902686): Support outputting human-readable format. + public void writeToStream(OutputStream out, Format format) + throws IOException, ParserConfigurationException, TransformerException { + var docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + var document = docBuilder.newDocument(); + + switch (format) { + case HUMAN_READABLE: + throw new IllegalArgumentException( + "Outputting human-readable format is not supported at this time."); + case ON_DEVICE: + for (var child : this.toOdDomElements(document)) { + document.appendChild(child); + } + break; + default: + throw new IllegalStateException("Unrecognized input format."); + } + + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + StreamResult streamResult = new StreamResult(out); // out + DOMSource domSource = new DOMSource(document); + transformer.transform(domSource, streamResult); + } + + /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */ + @Override + public List<Element> toOdDomElements(Document doc) { + Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE); + XmlUtils.appendChildren(aslEle, mSafetyLabels.toOdDomElements(doc)); + return List.of(aslEle); + } + + public static void test() { + // TODO(b/329902686): Add tests. + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java new file mode 100644 index 000000000000..7e7fcf9c08ba --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib; + +import com.android.asllib.util.MalformedXmlException; + +import org.w3c.dom.Element; + +import java.util.List; + +public class AndroidSafetyLabelFactory implements AslMarshallableFactory<AndroidSafetyLabel> { + + /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */ + @Override + public AndroidSafetyLabel createFromHrElements(List<Element> appMetadataBundles) + throws MalformedXmlException { + Element appMetadataBundlesEle = XmlUtils.getSingleElement(appMetadataBundles); + Element safetyLabelsEle = + XmlUtils.getSingleChildElement( + appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS); + SafetyLabels safetyLabels = + new SafetyLabelsFactory().createFromHrElements(List.of(safetyLabelsEle)); + return new AndroidSafetyLabel(safetyLabels); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java new file mode 100644 index 000000000000..4e64ab0c53c1 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; + +public interface AslMarshallable { + + /** Creates the on-device DOM element from the AslMarshallable Java Object. */ + List<Element> toOdDomElements(Document doc); +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java new file mode 100644 index 000000000000..b8f9f0ef6235 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib; + +import com.android.asllib.util.MalformedXmlException; + +import org.w3c.dom.Element; + +import java.util.List; + +public interface AslMarshallableFactory<T extends AslMarshallable> { + + /** Creates an {@link AslMarshallableFactory} from human-readable DOM element */ + T createFromHrElements(List<Element> elements) throws MalformedXmlException; +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java new file mode 100644 index 000000000000..e5ed63b74ebf --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.Map; + +/** + * Data usage category representation containing one or more {@link DataType}. Valid category keys + * are defined in {@link DataCategoryConstants}, each category has a valid set of types {@link + * DataType}, which are mapped in {@link DataTypeConstants} + */ +public class DataCategory implements AslMarshallable { + private final String mCategoryName; + private final Map<String, DataType> mDataTypes; + + public DataCategory(String categoryName, Map<String, DataType> dataTypes) { + this.mCategoryName = categoryName; + this.mDataTypes = dataTypes; + } + + public String getCategoryName() { + return mCategoryName; + } + + /** Return the type {@link Map} of String type key to {@link DataType} */ + + public Map<String, DataType> getDataTypes() { + return mDataTypes; + } + + /** Creates on-device DOM element(s) from the {@link DataCategory}. */ + @Override + public List<Element> toOdDomElements(Document doc) { + Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, this.getCategoryName()); + for (DataType dataType : mDataTypes.values()) { + XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc)); + } + return List.of(dataCategoryEle); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java new file mode 100644 index 000000000000..b364c8b37194 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib; + + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels}, + * {@link DataCategory}, and {@link DataType} + */ +public class DataCategoryConstants { + + public static final String CATEGORY_PERSONAL = "personal"; + public static final String CATEGORY_FINANCIAL = "financial"; + public static final String CATEGORY_LOCATION = "location"; + public static final String CATEGORY_EMAIL_TEXT_MESSAGE = "email_text_message"; + public static final String CATEGORY_PHOTO_VIDEO = "photo_video"; + public static final String CATEGORY_AUDIO = "audio"; + public static final String CATEGORY_STORAGE = "storage"; + public static final String CATEGORY_HEALTH_FITNESS = "health_fitness"; + public static final String CATEGORY_CONTACTS = "contacts"; + public static final String CATEGORY_CALENDAR = "calendar"; + public static final String CATEGORY_IDENTIFIERS = "identifiers"; + public static final String CATEGORY_APP_PERFORMANCE = "app_performance"; + public static final String CATEGORY_ACTIONS_IN_APP = "actions_in_app"; + public static final String CATEGORY_SEARCH_AND_BROWSING = "search_and_browsing"; + + /** Set of valid categories */ + public static final Set<String> VALID_CATEGORIES = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList( + CATEGORY_PERSONAL, + CATEGORY_FINANCIAL, + CATEGORY_LOCATION, + CATEGORY_EMAIL_TEXT_MESSAGE, + CATEGORY_PHOTO_VIDEO, + CATEGORY_AUDIO, + CATEGORY_STORAGE, + CATEGORY_HEALTH_FITNESS, + CATEGORY_CONTACTS, + CATEGORY_CALENDAR, + CATEGORY_IDENTIFIERS, + CATEGORY_APP_PERFORMANCE, + CATEGORY_ACTIONS_IN_APP, + CATEGORY_SEARCH_AND_BROWSING))); + + /** Returns {@link Set} of valid {@link String} category keys */ + public static Set<String> getValidDataCategories() { + return VALID_CATEGORIES; + } + + private DataCategoryConstants() { + /* do nothing - hide constructor */ + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java new file mode 100644 index 000000000000..d9463452d7bc --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib; + +import com.android.asllib.util.MalformedXmlException; + +import org.w3c.dom.Element; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DataCategoryFactory implements AslMarshallableFactory<DataCategory> { + @Override + public DataCategory createFromHrElements(List<Element> elements) throws MalformedXmlException { + String categoryName = null; + Map<String, DataType> dataTypeMap = new HashMap<String, DataType>(); + for (Element ele : elements) { + categoryName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY); + String dataTypeName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE); + if (!DataTypeConstants.getValidDataTypes().contains(dataTypeName)) { + throw new MalformedXmlException( + String.format("Unrecognized data type name: %s", dataTypeName)); + } + dataTypeMap.put(dataTypeName, new DataTypeFactory().createFromHrElements(List.of(ele))); + } + + return new DataCategory(categoryName, dataTypeMap); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java new file mode 100644 index 000000000000..d2fffc0a36f6 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.Map; + +/** + * Data label representation with data shared and data collected maps containing zero or more {@link + * DataCategory} + */ +public class DataLabels implements AslMarshallable { + private final Map<String, DataCategory> mDataAccessed; + private final Map<String, DataCategory> mDataCollected; + private final Map<String, DataCategory> mDataShared; + + public DataLabels( + Map<String, DataCategory> dataAccessed, + Map<String, DataCategory> dataCollected, + Map<String, DataCategory> dataShared) { + mDataAccessed = dataAccessed; + mDataCollected = dataCollected; + mDataShared = dataShared; + } + + /** + * Returns the data accessed {@link Map} of {@link com.android.asllib.DataCategoryConstants} to + * {@link DataCategory} + */ + public Map<String, DataCategory> getDataAccessed() { + return mDataAccessed; + } + + /** + * Returns the data collected {@link Map} of {@link com.android.asllib.DataCategoryConstants} to + * {@link DataCategory} + */ + public Map<String, DataCategory> getDataCollected() { + return mDataCollected; + } + + /** + * Returns the data shared {@link Map} of {@link com.android.asllib.DataCategoryConstants} to + * {@link DataCategory} + */ + public Map<String, DataCategory> getDataShared() { + return mDataShared; + } + + /** Gets the on-device DOM element for the {@link DataLabels}. */ + @Override + public List<Element> toOdDomElements(Document doc) { + Element dataLabelsEle = + XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS); + + maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_ACCESSED); + maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED); + maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED); + + return List.of(dataLabelsEle); + } + + private void maybeAppendDataUsages( + Document doc, + Element dataLabelsEle, + Map<String, DataCategory> dataCategoriesMap, + String dataUsageTypeName) { + if (dataCategoriesMap.isEmpty()) { + return; + } + Element dataUsageEle = XmlUtils.createPbundleEleWithName(doc, dataUsageTypeName); + + for (String dataCategoryName : dataCategoriesMap.keySet()) { + Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, dataCategoryName); + DataCategory dataCategory = dataCategoriesMap.get(dataCategoryName); + for (String dataTypeName : dataCategory.getDataTypes().keySet()) { + DataType dataType = dataCategory.getDataTypes().get(dataTypeName); + XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc)); + } + dataUsageEle.appendChild(dataCategoryEle); + } + dataLabelsEle.appendChild(dataUsageEle); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java new file mode 100644 index 000000000000..1adb140f446d --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib; + +import com.android.asllib.util.MalformedXmlException; + +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class DataLabelsFactory implements AslMarshallableFactory<DataLabels> { + + /** Creates a {@link DataLabels} from the human-readable DOM element. */ + @Override + public DataLabels createFromHrElements(List<Element> elements) throws MalformedXmlException { + Element ele = XmlUtils.getSingleElement(elements); + Map<String, DataCategory> dataAccessed = + getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_ACCESSED); + Map<String, DataCategory> dataCollected = + getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_COLLECTED); + Map<String, DataCategory> dataShared = + getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_SHARED); + + // Validate booleans such as isCollectionOptional, isSharingOptional. + for (DataCategory dataCategory : dataAccessed.values()) { + for (DataType dataType : dataCategory.getDataTypes().values()) { + if (dataType.getIsSharingOptional() != null) { + throw new MalformedXmlException( + String.format( + "isSharingOptional was unexpectedly defined on a DataType" + + " belonging to data accessed: %s", + dataType.getDataTypeName())); + } + if (dataType.getIsCollectionOptional() != null) { + throw new MalformedXmlException( + String.format( + "isCollectionOptional was unexpectedly defined on a DataType" + + " belonging to data accessed: %s", + dataType.getDataTypeName())); + } + } + } + for (DataCategory dataCategory : dataCollected.values()) { + for (DataType dataType : dataCategory.getDataTypes().values()) { + if (dataType.getIsSharingOptional() != null) { + throw new MalformedXmlException( + String.format( + "isSharingOptional was unexpectedly defined on a DataType" + + " belonging to data collected: %s", + dataType.getDataTypeName())); + } + } + } + for (DataCategory dataCategory : dataShared.values()) { + for (DataType dataType : dataCategory.getDataTypes().values()) { + if (dataType.getIsCollectionOptional() != null) { + throw new MalformedXmlException( + String.format( + "isCollectionOptional was unexpectedly defined on a DataType" + + " belonging to data shared: %s", + dataType.getDataTypeName())); + } + } + } + + return new DataLabels(dataAccessed, dataCollected, dataShared); + } + + private static Map<String, DataCategory> getDataCategoriesWithTag( + Element dataLabelsEle, String dataCategoryUsageTypeTag) throws MalformedXmlException { + NodeList dataUsedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag); + Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>(); + + Set<String> dataCategoryNames = new HashSet<String>(); + for (int i = 0; i < dataUsedNodeList.getLength(); i++) { + Element dataUsedEle = (Element) dataUsedNodeList.item(i); + String dataCategoryName = dataUsedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY); + if (!DataCategoryConstants.getValidDataCategories().contains(dataCategoryName)) { + throw new MalformedXmlException( + String.format("Unrecognized category name: %s", dataCategoryName)); + } + dataCategoryNames.add(dataCategoryName); + } + for (String dataCategoryName : dataCategoryNames) { + var dataCategoryElements = + XmlUtils.asElementList(dataUsedNodeList).stream() + .filter( + ele -> + ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY) + .equals(dataCategoryName)) + .toList(); + DataCategory dataCategory = + new DataCategoryFactory().createFromHrElements(dataCategoryElements); + dataCategoryMap.put(dataCategoryName, dataCategory); + } + return dataCategoryMap; + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java new file mode 100644 index 000000000000..5ba29757e19e --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.Set; + +/** + * Data usage type representation. Types are specific to a {@link DataCategory} and contains + * metadata related to the data usage purpose. + */ +public class DataType implements AslMarshallable { + + public enum Purpose { + PURPOSE_APP_FUNCTIONALITY(1), + PURPOSE_ANALYTICS(2), + PURPOSE_DEVELOPER_COMMUNICATIONS(3), + PURPOSE_FRAUD_PREVENTION_SECURITY(4), + PURPOSE_ADVERTISING(5), + PURPOSE_PERSONALIZATION(6), + PURPOSE_ACCOUNT_MANAGEMENT(7); + + private static final String PURPOSE_PREFIX = "PURPOSE_"; + + private final int mValue; + + Purpose(int value) { + this.mValue = value; + } + + /** Get the int value associated with the Purpose. */ + public int getValue() { + return mValue; + } + + /** Get the Purpose associated with the int value. */ + public static Purpose forValue(int value) { + for (Purpose e : values()) { + if (e.getValue() == value) { + return e; + } + } + throw new IllegalArgumentException("No enum for value: " + value); + } + + /** Get the Purpose associated with the human-readable String. */ + public static Purpose forString(String s) { + for (Purpose e : values()) { + if (e.toString().equals(s)) { + return e; + } + } + throw new IllegalArgumentException("No enum for str: " + s); + } + + /** Human-readable String representation of Purpose. */ + public String toString() { + if (!this.name().startsWith(PURPOSE_PREFIX)) { + return this.name(); + } + return this.name().substring(PURPOSE_PREFIX.length()).toLowerCase(); + } + } + + private final String mDataTypeName; + + private final Set<Purpose> mPurposeSet; + private final Boolean mIsCollectionOptional; + private final Boolean mIsSharingOptional; + private final Boolean mEphemeral; + + public DataType( + String dataTypeName, + Set<Purpose> purposeSet, + Boolean isCollectionOptional, + Boolean isSharingOptional, + Boolean ephemeral) { + this.mDataTypeName = dataTypeName; + this.mPurposeSet = purposeSet; + this.mIsCollectionOptional = isCollectionOptional; + this.mIsSharingOptional = isSharingOptional; + this.mEphemeral = ephemeral; + } + + public String getDataTypeName() { + return mDataTypeName; + } + + /** + * Returns {@link Set} of valid {@link Integer} purposes for using the associated data category + * and type + */ + public Set<Purpose> getPurposeSet() { + return mPurposeSet; + } + + /** + * For data-collected, returns {@code true} if data usage is user optional and {@code false} if + * data usage is required. Should return {@code null} for data-accessed and data-shared. + */ + public Boolean getIsCollectionOptional() { + return mIsCollectionOptional; + } + + /** + * For data-shared, returns {@code true} if data usage is user optional and {@code false} if + * data usage is required. Should return {@code null} for data-accessed and data-collected. + */ + public Boolean getIsSharingOptional() { + return mIsSharingOptional; + } + + /** + * For data-collected, returns {@code true} if data usage is user optional and {@code false} if + * data usage is processed ephemerally. Should return {@code null} for data-shared. + */ + public Boolean getEphemeral() { + return mEphemeral; + } + + @Override + public List<Element> toOdDomElements(Document doc) { + Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, this.getDataTypeName()); + if (!this.getPurposeSet().isEmpty()) { + Element purposesEle = doc.createElement(XmlUtils.OD_TAG_INT_ARRAY); + purposesEle.setAttribute(XmlUtils.OD_ATTR_NAME, XmlUtils.OD_NAME_PURPOSES); + purposesEle.setAttribute( + XmlUtils.OD_ATTR_NUM, String.valueOf(this.getPurposeSet().size())); + for (DataType.Purpose purpose : this.getPurposeSet()) { + Element purposeEle = doc.createElement(XmlUtils.OD_TAG_ITEM); + purposeEle.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(purpose.getValue())); + purposesEle.appendChild(purposeEle); + } + dataTypeEle.appendChild(purposesEle); + } + + maybeAddBoolToOdElement( + doc, + dataTypeEle, + this.getIsCollectionOptional(), + XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL); + maybeAddBoolToOdElement( + doc, + dataTypeEle, + this.getIsSharingOptional(), + XmlUtils.OD_NAME_IS_SHARING_OPTIONAL); + maybeAddBoolToOdElement(doc, dataTypeEle, this.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL); + return List.of(dataTypeEle); + } + + private static void maybeAddBoolToOdElement( + Document doc, Element parentEle, Boolean b, String odName) { + if (b == null) { + return; + } + Element ele = XmlUtils.createOdBooleanEle(doc, odName, b); + parentEle.appendChild(ele); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java new file mode 100644 index 000000000000..a0a75377e988 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels}, + * {@link DataCategory}, and {@link DataType} + */ +public class DataTypeConstants { + /** Data types for {@link DataCategoryConstants.CATEGORY_PERSONAL} */ + public static final String TYPE_NAME = "name"; + + public static final String TYPE_EMAIL_ADDRESS = "email_address"; + public static final String TYPE_PHONE_NUMBER = "phone_number"; + public static final String TYPE_RACE_ETHNICITY = "race_ethnicity"; + public static final String TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS = + "political_or_religious_beliefs"; + public static final String TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY = + "sexual_orientation_or_gender_identity"; + public static final String TYPE_PERSONAL_IDENTIFIERS = "personal_identifiers"; + public static final String TYPE_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_FINANCIAL} */ + public static final String TYPE_CARD_BANK_ACCOUNT = "card_bank_account"; + + public static final String TYPE_PURCHASE_HISTORY = "purchase_history"; + public static final String TYPE_CREDIT_SCORE = "credit_score"; + public static final String TYPE_FINANCIAL_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_LOCATION} */ + public static final String TYPE_APPROX_LOCATION = "approx_location"; + + public static final String TYPE_PRECISE_LOCATION = "precise_location"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_EMAIL_TEXT_MESSAGE} */ + public static final String TYPE_EMAILS = "emails"; + + public static final String TYPE_TEXT_MESSAGES = "text_messages"; + public static final String TYPE_EMAIL_TEXT_MESSAGE_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_PHOTO_VIDEO} */ + public static final String TYPE_PHOTOS = "photos"; + + public static final String TYPE_VIDEOS = "videos"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_AUDIO} */ + public static final String TYPE_SOUND_RECORDINGS = "sound_recordings"; + + public static final String TYPE_MUSIC_FILES = "music_files"; + public static final String TYPE_AUDIO_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_STORAGE} */ + public static final String TYPE_FILES_DOCS = "files_docs"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_HEALTH_FITNESS} */ + public static final String TYPE_HEALTH = "health"; + + public static final String TYPE_FITNESS = "fitness"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_CONTACTS} */ + public static final String TYPE_CONTACTS = "contacts"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_CALENDAR} */ + public static final String TYPE_CALENDAR = "calendar"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_IDENTIFIERS} */ + public static final String TYPE_IDENTIFIERS_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_APP_PERFORMANCE} */ + public static final String TYPE_CRASH_LOGS = "crash_logs"; + + public static final String TYPE_PERFORMANCE_DIAGNOSTICS = "performance_diagnostics"; + public static final String TYPE_APP_PERFORMANCE_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_ACTIONS_IN_APP} */ + public static final String TYPE_USER_INTERACTION = "user_interaction"; + + public static final String TYPE_IN_APP_SEARCH_HISTORY = "in_app_search_history"; + public static final String TYPE_INSTALLED_APPS = "installed_apps"; + public static final String TYPE_USER_GENERATED_CONTENT = "user_generated_content"; + public static final String TYPE_ACTIONS_IN_APP_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_SEARCH_AND_BROWSING} */ + public static final String TYPE_WEB_BROWSING_HISTORY = "web_browsing_history"; + + /** Set of valid categories */ + public static final Set<String> VALID_TYPES = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList( + TYPE_NAME, + TYPE_EMAIL_ADDRESS, + TYPE_PHONE_NUMBER, + TYPE_RACE_ETHNICITY, + TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS, + TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY, + TYPE_PERSONAL_IDENTIFIERS, + TYPE_OTHER, + TYPE_CARD_BANK_ACCOUNT, + TYPE_PURCHASE_HISTORY, + TYPE_CREDIT_SCORE, + TYPE_FINANCIAL_OTHER, + TYPE_APPROX_LOCATION, + TYPE_PRECISE_LOCATION, + TYPE_EMAILS, + TYPE_TEXT_MESSAGES, + TYPE_EMAIL_TEXT_MESSAGE_OTHER, + TYPE_PHOTOS, + TYPE_VIDEOS, + TYPE_SOUND_RECORDINGS, + TYPE_MUSIC_FILES, + TYPE_AUDIO_OTHER, + TYPE_FILES_DOCS, + TYPE_HEALTH, + TYPE_FITNESS, + TYPE_CONTACTS, + TYPE_CALENDAR, + TYPE_IDENTIFIERS_OTHER, + TYPE_CRASH_LOGS, + TYPE_PERFORMANCE_DIAGNOSTICS, + TYPE_APP_PERFORMANCE_OTHER, + TYPE_USER_INTERACTION, + TYPE_IN_APP_SEARCH_HISTORY, + TYPE_INSTALLED_APPS, + TYPE_USER_GENERATED_CONTENT, + TYPE_ACTIONS_IN_APP_OTHER, + TYPE_WEB_BROWSING_HISTORY))); + + /** Returns {@link Set} of valid {@link String} category keys */ + public static Set<String> getValidDataTypes() { + return VALID_TYPES; + } + + private DataTypeConstants() { + /* do nothing - hide constructor */ + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java new file mode 100644 index 000000000000..e3d1587d860c --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib; + +import org.w3c.dom.Element; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class DataTypeFactory implements AslMarshallableFactory<DataType> { + /** Creates a {@link DataType} from the human-readable DOM element. */ + @Override + public DataType createFromHrElements(List<Element> elements) { + Element hrDataTypeEle = XmlUtils.getSingleElement(elements); + String dataTypeName = hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE); + Set<DataType.Purpose> purposeSet = + Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|")) + .map(DataType.Purpose::forString) + .collect(Collectors.toUnmodifiableSet()); + Boolean isCollectionOptional = + XmlUtils.fromString( + hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL)); + Boolean isSharingOptional = + XmlUtils.fromString( + hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL)); + Boolean ephemeral = + XmlUtils.fromString(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_EPHEMERAL)); + return new DataType( + dataTypeName, purposeSet, isCollectionOptional, isSharingOptional, ephemeral); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java new file mode 100644 index 000000000000..f06522fc2a5c --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; + +/** Safety Label representation containing zero or more {@link DataCategory} for data shared */ +public class SafetyLabels implements AslMarshallable { + + private final Long mVersion; + private final DataLabels mDataLabels; + + public SafetyLabels(Long version, DataLabels dataLabels) { + this.mVersion = version; + this.mDataLabels = dataLabels; + } + + /** Returns the data label for the safety label */ + public DataLabels getDataLabel() { + return mDataLabels; + } + + /** Gets the version of the {@link SafetyLabels}. */ + public Long getVersion() { + return mVersion; + } + + /** Creates an on-device DOM element from the {@link SafetyLabels}. */ + @Override + public List<Element> toOdDomElements(Document doc) { + Element safetyLabelsEle = + XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS); + XmlUtils.appendChildren(safetyLabelsEle, mDataLabels.toOdDomElements(doc)); + return List.of(safetyLabelsEle); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java new file mode 100644 index 000000000000..80b9f5783b9d --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib; + +import com.android.asllib.util.MalformedXmlException; + +import org.w3c.dom.Element; + +import java.util.List; + +public class SafetyLabelsFactory implements AslMarshallableFactory<SafetyLabels> { + + /** Creates a {@link SafetyLabels} from the human-readable DOM element. */ + @Override + public SafetyLabels createFromHrElements(List<Element> elements) throws MalformedXmlException { + Element safetyLabelsEle = XmlUtils.getSingleElement(elements); + Long version; + try { + version = Long.parseLong(safetyLabelsEle.getAttribute(XmlUtils.HR_ATTR_VERSION)); + } catch (Exception e) { + throw new IllegalArgumentException( + "Malformed or missing required version in safety labels."); + } + + DataLabels dataLabels = + new DataLabelsFactory() + .createFromHrElements( + List.of( + XmlUtils.getSingleChildElement( + safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS))); + return new SafetyLabels(version, dataLabels); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java new file mode 100644 index 000000000000..3bc9ccc2138b --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.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 com.android.asllib; + +import com.android.asllib.util.MalformedXmlException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; +import java.util.List; + +public class XmlUtils { + public static final String HR_TAG_APP_METADATA_BUNDLES = "app-metadata-bundles"; + public static final String HR_TAG_SAFETY_LABELS = "safety-labels"; + public static final String HR_TAG_DATA_LABELS = "data-labels"; + public static final String HR_TAG_DATA_ACCESSED = "data-accessed"; + public static final String HR_TAG_DATA_COLLECTED = "data-collected"; + public static final String HR_TAG_DATA_SHARED = "data-shared"; + + public static final String HR_ATTR_DATA_CATEGORY = "dataCategory"; + public static final String HR_ATTR_DATA_TYPE = "dataType"; + public static final String HR_ATTR_IS_COLLECTION_OPTIONAL = "isCollectionOptional"; + public static final String HR_ATTR_IS_SHARING_OPTIONAL = "isSharingOptional"; + public static final String HR_ATTR_EPHEMERAL = "ephemeral"; + public static final String HR_ATTR_PURPOSES = "purposes"; + public static final String HR_ATTR_VERSION = "version"; + + public static final String OD_TAG_BUNDLE = "bundle"; + public static final String OD_TAG_PBUNDLE_AS_MAP = "pbundle_as_map"; + public static final String OD_TAG_BOOLEAN = "boolean"; + public static final String OD_TAG_INT_ARRAY = "int-array"; + public static final String OD_TAG_ITEM = "item"; + public static final String OD_ATTR_NAME = "name"; + public static final String OD_ATTR_VALUE = "value"; + public static final String OD_ATTR_NUM = "num"; + public static final String OD_NAME_SAFETY_LABELS = "safety_labels"; + public static final String OD_NAME_DATA_LABELS = "data_labels"; + public static final String OD_NAME_DATA_ACCESSED = "data_accessed"; + public static final String OD_NAME_DATA_COLLECTED = "data_collected"; + public static final String OD_NAME_DATA_SHARED = "data_shared"; + public static final String OD_NAME_PURPOSES = "purposes"; + public static final String OD_NAME_IS_COLLECTION_OPTIONAL = "is_collection_optional"; + public static final String OD_NAME_IS_SHARING_OPTIONAL = "is_sharing_optional"; + public static final String OD_NAME_EPHEMERAL = "ephemeral"; + + public static final String TRUE_STR = "true"; + public static final String FALSE_STR = "false"; + + /** Gets the single top-level {@link Element} having the {@param tagName}. */ + public static Element getSingleElement(Document doc, String tagName) + throws MalformedXmlException { + var elements = doc.getElementsByTagName(tagName); + return getSingleElement(elements, tagName); + } + + /** + * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}. + */ + public static Element getSingleChildElement(Element parentEle, String tagName) + throws MalformedXmlException { + var elements = parentEle.getElementsByTagName(tagName); + return getSingleElement(elements, tagName); + } + + /** Gets the single {@link Element} from {@param elements} */ + public static Element getSingleElement(NodeList elements, String tagName) + throws MalformedXmlException { + if (elements.getLength() != 1) { + throw new MalformedXmlException( + String.format( + "Expected 1 element \"%s\" in NodeList but got %s.", + tagName, elements.getLength())); + } + var elementAsNode = elements.item(0); + if (!(elementAsNode instanceof Element)) { + throw new MalformedXmlException( + String.format("%s was not a valid XML element.", tagName)); + } + return ((Element) elementAsNode); + } + + /** Gets the single {@link Element} within {@param elements}. */ + public static Element getSingleElement(List<Element> elements) { + if (elements.size() != 1) { + throw new IllegalStateException( + String.format("Expected 1 element in list but got %s.", elements.size())); + } + return elements.get(0); + } + + /** Converts {@param nodeList} into List of {@link Element}. */ + public static List<Element> asElementList(NodeList nodeList) { + List<Element> elementList = new ArrayList<Element>(); + for (int i = 0; i < nodeList.getLength(); i++) { + var elementAsNode = nodeList.item(0); + if (elementAsNode instanceof Element) { + elementList.add(((Element) elementAsNode)); + } + } + return elementList; + } + + /** Appends {@param children} to the {@param ele}. */ + public static void appendChildren(Element ele, List<Element> children) { + for (Element c : children) { + ele.appendChild(c); + } + } + + /** Gets the Boolean from the String value. */ + public static Boolean fromString(String s) { + if (s == null) { + return null; + } + if (s.equals(TRUE_STR)) { + return true; + } else if (s.equals(FALSE_STR)) { + return false; + } + return null; + } + + /** Creates an on-device PBundle DOM Element with the given attribute name. */ + public static Element createPbundleEleWithName(Document doc, String name) { + var ele = doc.createElement(XmlUtils.OD_TAG_PBUNDLE_AS_MAP); + ele.setAttribute(XmlUtils.OD_ATTR_NAME, name); + return ele; + } + + /** Create an on-device Boolean DOM Element with the given attribute name. */ + public static Element createOdBooleanEle(Document doc, String name, boolean b) { + var ele = doc.createElement(XmlUtils.OD_TAG_BOOLEAN); + ele.setAttribute(XmlUtils.OD_ATTR_NAME, name); + ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(b)); + return ele; + } + + /** Returns whether the String is null or empty. */ + public static boolean isNullOrEmpty(String s) { + return s == null || s.isEmpty(); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/MalformedXmlException.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/MalformedXmlException.java new file mode 100644 index 000000000000..216df56c453e --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/MalformedXmlException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.asllib.util; + +public class MalformedXmlException extends Exception { + /** Constructs an {@code MalformedXmlException} with no detail message. */ + public MalformedXmlException() { + super(); + } + + /** + * Constructs an {@code MalformedXmlException} with the specified detail message. + * + * @param s the detail message. + */ + public MalformedXmlException(String s) { + super(s); + } +} diff --git a/wifi/java/src/android/net/wifi/WifiBlobStore.java b/wifi/java/src/android/net/wifi/WifiBlobStore.java new file mode 100644 index 000000000000..8bfaae72f932 --- /dev/null +++ b/wifi/java/src/android/net/wifi/WifiBlobStore.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.wifi; + +import com.android.internal.net.ConnectivityBlobStore; + +/** + * Database blob store for Wifi. + * @hide + */ +public class WifiBlobStore extends ConnectivityBlobStore { + private static final String DB_NAME = "WifiBlobStore.db"; + private static WifiBlobStore sInstance; + private WifiBlobStore() { + super(DB_NAME); + } + + /** Returns an instance of WifiBlobStore. */ + public static WifiBlobStore getInstance() { + if (sInstance == null) { + sInstance = new WifiBlobStore(); + } + return sInstance; + } +} diff --git a/wifi/java/src/android/net/wifi/WifiKeystore.java b/wifi/java/src/android/net/wifi/WifiKeystore.java index 1cda0326bf6c..a06d0eeade72 100644 --- a/wifi/java/src/android/net/wifi/WifiKeystore.java +++ b/wifi/java/src/android/net/wifi/WifiKeystore.java @@ -18,12 +18,17 @@ package android.net.wifi; import android.annotation.NonNull; import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.os.Binder; import android.os.Process; import android.os.ServiceManager; import android.os.ServiceSpecificException; import android.security.legacykeystore.ILegacyKeystore; import android.util.Log; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + /** * This class allows the storage and retrieval of non-standard Wifi certificate blobs. * @hide @@ -34,7 +39,7 @@ public final class WifiKeystore { private static final String TAG = "WifiKeystore"; private static final String LEGACY_KEYSTORE_SERVICE_NAME = "android.security.legacykeystore"; - private static ILegacyKeystore getService() { + private static ILegacyKeystore getLegacyKeystore() { return ILegacyKeystore.Stub.asInterface( ServiceManager.checkService(LEGACY_KEYSTORE_SERVICE_NAME)); } @@ -54,13 +59,18 @@ public final class WifiKeystore { @SystemApi @SuppressLint("UnflaggedApi") public static boolean put(@NonNull String alias, @NonNull byte[] blob) { + // ConnectivityBlobStore uses the calling uid as a key into the DB. + // Clear identity to ensure that callers from system apps and the Wifi framework + // are able to access the same values. + final long identity = Binder.clearCallingIdentity(); try { Log.i(TAG, "put blob. alias " + alias); - getService().put(alias, Process.WIFI_UID, blob); - return true; + return WifiBlobStore.getInstance().put(alias, blob); } catch (Exception e) { Log.e(TAG, "Failed to put blob.", e); return false; + } finally { + Binder.restoreCallingIdentity(identity); } } @@ -69,23 +79,31 @@ public final class WifiKeystore { * @param alias Name of the blob to retrieve. * @return The unstructured blob, that is the blob that was stored using * {@link android.net.wifi.WifiKeystore#put}. - * Returns null if no blob was found. + * Returns empty byte[] if no blob was found. * @hide */ @SystemApi @SuppressLint("UnflaggedApi") public static @NonNull byte[] get(@NonNull String alias) { + final long identity = Binder.clearCallingIdentity(); try { Log.i(TAG, "get blob. alias " + alias); - return getService().get(alias, Process.WIFI_UID); + byte[] blob = WifiBlobStore.getInstance().get(alias); + if (blob != null) { + return blob; + } + Log.i(TAG, "Searching for blob in Legacy Keystore"); + return getLegacyKeystore().get(alias, Process.WIFI_UID); } catch (ServiceSpecificException e) { if (e.errorCode != ILegacyKeystore.ERROR_ENTRY_NOT_FOUND) { Log.e(TAG, "Failed to get blob.", e); } } catch (Exception e) { Log.e(TAG, "Failed to get blob.", e); + } finally { + Binder.restoreCallingIdentity(identity); } - return null; + return new byte[0]; } /** @@ -97,17 +115,27 @@ public final class WifiKeystore { @SystemApi @SuppressLint("UnflaggedApi") public static boolean remove(@NonNull String alias) { + boolean blobStoreSuccess = false; + boolean legacyKsSuccess = false; + final long identity = Binder.clearCallingIdentity(); try { - getService().remove(alias, Process.WIFI_UID); - return true; + Log.i(TAG, "remove blob. alias " + alias); + blobStoreSuccess = WifiBlobStore.getInstance().remove(alias); + // Legacy Keystore will throw an exception if the alias is not found. + getLegacyKeystore().remove(alias, Process.WIFI_UID); + legacyKsSuccess = true; } catch (ServiceSpecificException e) { if (e.errorCode != ILegacyKeystore.ERROR_ENTRY_NOT_FOUND) { Log.e(TAG, "Failed to remove blob.", e); } } catch (Exception e) { Log.e(TAG, "Failed to remove blob.", e); + } finally { + Binder.restoreCallingIdentity(identity); } - return false; + Log.i(TAG, "Removal status: wifiBlobStore=" + blobStoreSuccess + + ", legacyKeystore=" + legacyKsSuccess); + return blobStoreSuccess || legacyKsSuccess; } /** @@ -119,14 +147,24 @@ public final class WifiKeystore { @SystemApi @SuppressLint("UnflaggedApi") public static @NonNull String[] list(@NonNull String prefix) { + final long identity = Binder.clearCallingIdentity(); try { - final String[] aliases = getService().list(prefix, Process.WIFI_UID); - for (int i = 0; i < aliases.length; ++i) { - aliases[i] = aliases[i].substring(prefix.length()); + // Aliases from WifiBlobStore will be pre-trimmed. + final String[] blobStoreAliases = WifiBlobStore.getInstance().list(prefix); + final String[] legacyAliases = getLegacyKeystore().list(prefix, Process.WIFI_UID); + for (int i = 0; i < legacyAliases.length; ++i) { + legacyAliases[i] = legacyAliases[i].substring(prefix.length()); } - return aliases; + // Deduplicate aliases before returning. + Set<String> uniqueAliases = new HashSet<>(); + uniqueAliases.addAll(Arrays.asList(blobStoreAliases)); + uniqueAliases.addAll(Arrays.asList(legacyAliases)); + String[] uniqueAliasArray = new String[uniqueAliases.size()]; + return uniqueAliases.toArray(uniqueAliasArray); } catch (Exception e) { Log.e(TAG, "Failed to list blobs.", e); + } finally { + Binder.restoreCallingIdentity(identity); } return new String[0]; } diff --git a/wifi/wifi.aconfig b/wifi/wifi.aconfig index 6ac986e406a0..6c4e4c3eb9be 100644 --- a/wifi/wifi.aconfig +++ b/wifi/wifi.aconfig @@ -2,6 +2,7 @@ package: "android.net.wifi.flags" flag { name: "get_device_cross_akm_roaming_support" + is_exported: true namespace: "wifi" description: "Add new API to get the device support for CROSS-AKM roaming" bug: "313038031" |